Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

swift : shortcut for guard "let self = ..."?

I have to parse json server response into a swift object. I use this code :

struct MyGPSCoords {

    var latitude:Double
    var longitude:Double
    var accuracy:Int
    var datetime:NSDate

    init?(infobrutFromJson_:[String:String]?)
    {
        guard let infobrut = infobrutFromJson_ else {
            // first time the user sign up to the app, php server returns "null" in Json 
            return nil
        }

        guard
        let lat:Double = Double(infobrut["latitude"] ?? "nil"),
        let lng = Double(infobrut["longitude"] ?? "nil"),
        let acc = Int(infobrut["accuracy"] ?? "nil"),
        let dtm = NSDate(timeIntervalSince1970: Double(infobrut["time"] ?? "nil"))
        else {
            print("warning : unable to parse data from server. Returning nil");
            return nil ; // position not NIL but format not expected => = nil
        }
        self.latitude = lat
        self.longitude = lng
        self.accuracy = acc
        self.datetime = dtm
    }


}

I want to make the "guard" statement as short as possible. First, I added ?? "nil" so if one of the keys doesn't exist, Double("nil") gets nil and guard statement can handle. For NSDate, I made an extension with a convenience init? returning nil if its input is nil, so I can do the same.

My question is, can i do it even shorter by assigning directly to self.latitude the values right in the guard statement ? I tried that :

guard self.latitude = Double(infobrut["latitude"] ?? "nil"), ... 

It says it cannot cast from Double? to Double. Is there any way to make this guard even shorter and avoiding me to assign lat, lng, acc and dtm buffering variables ?

like image 776
Jerem Lachkar Avatar asked Sep 02 '25 13:09

Jerem Lachkar


1 Answers

First, you should of course try to fix the JSON, since this JSON is malformed. Strings are not numbers in JSON. Assuming you cannot correct this broken JSON, the tool you want is flatMap, which converts T?? to T? (which is what guard-let expects).

guard
    let lat = infobrut["latitude"].flatMap(Double.init),
    let lng = infobrut["longitude"].flatMap(Double.init),
    let acc = infobrut["accuracy"].flatMap(Int.init),
    let dtm = infobrut["time"].flatMap(TimeInterval.init).flatMap(Date.init(timeIntervalSince1970:))
    else {
        print("warning : unable to parse data from server. Returning nil")
        return nil // position not NIL but format not expected => = nil
}

I saw a lot of comments that Codable won't work here, but it absolutely will, and it's really what you should use. Here's one way (this is a little sloppy about its error messages, but it's simple):

struct MyGPSCoords: Decodable {

    var latitude:Double
    var longitude:Double
    var accuracy:Int
    var datetime:Date

    enum CodingKeys: String, CodingKey {
        case latitude, longitude, accuracy, datetime
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        guard
            let lat = Double(try container.decode(String.self, forKey: .latitude)),
            let lng = Double(try container.decode(String.self, forKey: .longitude)),
            let acc = Int(try container.decode(String.self, forKey: .accuracy)),
            let dtm = TimeInterval(try container.decode(String.self,
                                                        forKey: .datetime)).flatMap(Date.init(timeIntervalSince1970:))
        else {
            throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Could not decode"))
        }

        self.latitude = lat
        self.longitude = lng
        self.accuracy = acc
        self.datetime = dtm
    }

}

Or you can get really fancy with an internal helpful function and get rid of all the temporary variables and optionals through the power of throws.

struct MyGPSCoords: Decodable {

    var latitude:Double
    var longitude:Double
    var accuracy:Int
    var datetime:Date

    enum CodingKeys: String, CodingKey {
        case latitude, longitude, accuracy, datetime
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        func decodeBrokenJSON<T>(_ type: T.Type,
                                 forKey key: CodingKeys) throws -> T
            where T: Decodable & LosslessStringConvertible {
                return try T.init(container.decode(String.self, forKey: key)) ?? {
                    throw DecodingError.dataCorruptedError(forKey: key,
                                                           in: container,
                                                           debugDescription: "Could not decode \(key)")
                    }()
        }

        self.latitude = try decodeBrokenJSON(Double.self, forKey: .latitude)
        self.longitude = try decodeBrokenJSON(Double.self, forKey: .longitude)
        self.accuracy = try decodeBrokenJSON(Int.self, forKey: .accuracy)
        self.datetime = Date(timeIntervalSince1970: try decodeBrokenJSON(TimeInterval.self, forKey: .datetime))
    }

}

(IMO, this is a great example of how throws really shines and should be used much more than it commonly is.)

like image 143
Rob Napier Avatar answered Sep 05 '25 03:09

Rob Napier