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

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 ?

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.)

What you want to do is not possible. The compiler already tells you so, even though the error message is a bit misleading. You can either use a guard let that creates a new variable, or you can use a guard with a boolean expression. In your case there is no let so the compiler tries to parse a boolean expression. Instead it sees the assignment and produces the error message that the types don’t match. If the types would match (as in guard self.latitude = 12.0) the error message would be clearer: error: use of '=' in a boolean context, did you mean '=='?

The other solutions seem overly complicated. Simply make it
struct MyGPSCoords: Codable {
var latitude: Double?
var longitude: Double?
var accuracy: Int?
var datetime: Date?
var isValid {
return [latitude, longitude, accuracy, datetime].filter { $0 == nil }.isEmpty
}
}
// jsonData is whatever payload you get back from the URL request.
let coords = JSONDecoder().decode(jsonData, type: MyGPSCoords.self)
if !coords.isValid {
print("warning : unable to parse data from server.")
}
Since all of your properties are Optional, parsing can't fail if one or more of them is missing. The isValid check is much simpler than the guard let... clause in your original code.
EDIT: If, as Rob Napier suggests, all the JSON values are encoded as Strings, then here's another way to structure your MyGPSCoords:
struct MyGPSCoords: Codable {
// These are the Codable properties
fileprivate var latitudeString: String?
fileprivate var longitudeString: String?
fileprivate var accuracyString: String?
fileprivate var datetimeString: String?
// Default constant to use as a default check for validity
let invalid = Double.leastNonzeroMagnitude
// And these are the derived properties that you want users to use
var latitude: Double {
return Double(latitudeString ?? "\(invalid)") ?? invalid
}
var longitude: Double {
return Double(longitudeString ?? "\(invalid)") ?? invalid
}
var accuracy: Int {
return Int(accuracyString ?? "\(invalid)") ?? Int(invalid)
}
var date: Date {
return <whatever-formatter-output-you-need>
}
var isValid {
return [latitudeString, longitudeString, accuracyString, datetimeString].filter { $0 == nil }.isEmpty
&& latitude != invalid && longitude != invalid
&& accuracy != Int(invalid) /* && however you compare dates */
}
}

I know that question is old but I have to admit that I didn't understand very well the in-built Decodable/Decoder system in Swift (especially this notion of "Container", can't figure out what it represents exactly)
Anyway, I made my own decoder which enable to handle this situation in quite the same way as Android does (to decode JSONObject). I made an extension of Dictionary like this :
protocol Decodable {
init(from raw:[String:Any]) throws
}
extension Dictionary where Key == String
{
enum DecodableError : Error {
case unknownKey(key:String)
case keyWrongType(key:String, expectedType:String, actualValue:String)
case nullValueAtKey(key:String)
}
func getSafe<T>(_ key:String, forType t:T.Type) throws -> T
{
if(self[key] != nil)
{
if(self[key] is NSNull) // corresponds to the JSON null value (by experience)
{
throw DecodableError.nullValueAtKey(key:key)
}
else if(self[key] is T) // for raw type
{
return self[key] as! T
}
// try to parse self[key] to provided type if it's decodable
else if(self[key] is [String:Any] && t is Decodable.Type)
{
return try (t as! Decodable.Type).init(from: self[key] as! [String:Any]) as! T
}
throw DecodableError.keyWrongType(key: key,
expectedType: String(describing: T.self),
actualValue: String(describing:self[key]!))
}
throw DecodableError.unknownKey(key:key)
}
func getSafeOpt<T>(_ key:String, forType t:T.Type) throws -> T?
{
if(self[key] != nil)
{
if(self[key] is NSNull)
{
return nil
}
return try getSafe(key, forType: t)
}
throw DecodableError.unknownKey(key:key)
}
}
I use it like that :
struct Position : Decodable {
let latitude:Double
let longitude:Double
let accuracy:Int?
let member:Member
init(from raw:[String:Any]) throws
{
// getSafe throw exception whenever node are json<null> or if node doesn't exist
latitude = try raw.getSafe("lat", forType: Double.self)
longitude = try raw.getSafe("lng", forType: Double.self)
// getSafeOpt returns nil if the JSON value of node acc is null,
// but it still throw an exception if there is no "acc" node
accuracy = try raw.getSafeOpt("acc", forType: Int.self)
// you can use it to decode other objects that implement my Decodable protocol too :
member = try raw.getSafeOpt("member", forType: Member.self)
}
}
do {
try app.position = Position(from: rawDic)
}
catch {
print("Unable to parse Position : \(error) ")
return
}
This does not handle yet the JSON arrays, I'll do it later, or feel free to update my answer if you wish to add a JSON array handling mechanism.

Related

App crashes when setting custom Struct value in AppStorage

I have a custom struct I want to store in AppStorage:
struct Budget: Codable, RawRepresentable {
enum CodingKeys: String, CodingKey {
case total, spent
}
var total: Double
var spent: Double
init(total: Double = 5000.0, spent: Double = 3000.0) {
self.total = total
self.spent = spent
}
init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(Budget.self, from: data)
else { return nil }
self = result
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return ""
}
return result
}
}
I then have the following view:
struct DemoView: View {
#AppStorage(UserDefaults.StorageKeys.budget.rawValue) var budget = Budget()
var body: some View {
Button("Update") {
budget.total = 10
}
}
}
When I tap the button the app crashes with Thread 1: EXC_BAD_ACCESS on guard let data = try? JSONEncoder().encode(self) for rawValue in Budget. What am I doing wrong here?
You are running into infinite recursion. This is because types that conforms to both Encodable and RawRepresentable automatically get this encode(to:) implementation (source), which encodes the raw value. This means that when you call JSONEncoder().encode, it would try to call the getter of rawValue, which calls JSONEncoder().encode, forming infinite recursion.
To solve this, you can implement encode(to:) explicitly:
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(total, forKey: .total)
try container.encode(spent, forKey: .spent)
}
Note that you should also implement init(from:) explicitly, because you also get a init(from:) implementation (source) that tries to decode your JSON as a single JSON string, which you certainly do not want.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
total = try container.decode(Double.self, forKey: .total)
spent = try container.decode(Double.self, forKey: .spent)
}

working with similar json objects, casting int to string [duplicate]

This question already has answers here:
Using codable with value that is sometimes an Int and other times a String
(5 answers)
Closed last year.
i am working with a service that have both a websocket for live data and a api for historical data
the JSON looks similar and i would like to decode it to the same object
the only difference is that in the live one variable is a number but as a string and with the historical data the number is an int.
and preferably i would like to not have to create 2 almost identical decodable objects.
have anyone tried something similar.
You have to define a single type (Int or String) for your data structure and use init with Decoder to make a custom parsing.
struct MyData: Decodable {
let value: Int // Could be Int or String from different services
}
extension MyData {
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
value = try container.decode(Int.self, forKey: .value)
} catch {
let stringValue = try container.decode(String.self, forKey: .value)
if let valueInt = Int(stringValue) {
value = valueInt
} else {
var codingPath = container.codingPath
codingPath.append(CodingKeys.value)
let debugDescription = "Could not create Int from String \(stringValue) of field \(CodingKeys.value.rawValue)"
let context = DecodingError.Context(codingPath: codingPath, debugDescription: debugDescription)
throw DecodingError.dataCorrupted(context)
}
}
}
}
I think you need a wrapper for that case of some sort. To make it as convenient as possible you could use a property wrapper for this
#propertyWrapper
struct NormalOrStringyInt: Decodable {
var wrappedValue: Int?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let value = try? container.decode(Int.self) {
wrappedValue = value
} else if
let string = try? container.decode(String.self),
let value = Int(string)
{
wrappedValue = value
} else {
wrappedValue = nil // default value
}
}
}
struct Model: Codable {
#NormalOrStringyInt var id: Int?
var someInt: Int
var someString: String
...
}
let model = try! JSONDecoder().decode(Model, from: data)
let id: Int? = model.id.wrappedValue

Swift 4 - Convert 1 or 0 from mysql to bool

I have this variable here, its an Any
item["completed"]
Its either 1 or 0 which is true or false from mysql database, how can I covert the 1 or 0 to true or false?
I have tried this:
(item["completed"] as! NSNumber).boolValue)
but I get this error
Could not cast value of type 'NSTaggedPointerString' (0x10f906560) to
'NSNumber' (0x10e455d40).
You can try below extension which i have implemented,
extension String {
var bool: Bool {
return self == "1" ? true : false
}
}
(item["completed"] as? String).bool
Hope this way may help you.
Just use
let isCompleted = "1" == (item["completed"] as? String)
If you are using Decodable for json parsing, you can use following ApiBool struct instead of Bool
struct ApiBool: Codable {
let value: Bool
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self.value = string == "1"
} else {
let intVal = try container.decode(Int.self)
self.value = intVal == 1
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(self.value ? 1 : 0)
}
}
Note As mentioned in comments, you got an String data type and it is expected to be an integer, this is caused by libmysql driver, you can connect to MySQL using mysqlnd driver instead, this will result in a JSON with proper numeric data types, following posts might help you configure the driver
https://stackoverflow.com/a/46690317/1244597
https://stackoverflow.com/a/45366230/1244597
#ahemadabbas-vagh shared a good and reusable approach. But if we talk about computer science, a string's Boolean equality should not be measured like that.
This approach is the same as his, but it asks Bool if the value is true or false, not to String. Bool should know is a value is true or false, not String.
var str = "1"
extension Bool {
static func from(stringValue str: String) -> Bool {
return str == "1"
}
}
if Bool.from(stringValue: str) {
print("TRUE")
} else {
print("FALSE")
}
But it is still not correct. In computer science, null/nil or/and an empty string should be false and any other strings should be true. You can add the string "0" to this set.
You can implement like this; better approaches still might exist.
The idea is; globally a string can only be converted to true if it is not "0", not "" (empty string), not null/nil or if it is "true".
var str = "1" // check for nil, empty string and zero; these should be false; any other string should be true.
extension Bool {
static func from(stringValue str: String) -> Bool {
if str == "true" {
return true
}
guard let intVal = Int(str) else {
return false
}
return Bool(truncating: intVal as NSNumber)
}
}
if Bool.from(stringValue: str) {
print("TRUE")
} else {
print("FALSE")
}
If you use Decodable protocol; I know you know how to handle but:
struct aStruct: Decodable {
let boolVal: Bool
enum CodingKeys: String, CodingKey {
case completed
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let boolString = try container.decode(String.self, forKey: .completed)
self.boolVal = Bool.from(stringValue: boolString)
}
}

check the json response is array or int or string for a key?

I am having the json response in which "products" key sometime having the int value and some cases it had an array?
How to check whether it is having array or Int?
"products": 25
or
"products": [77,80,81,86]
I am using this
self.productsCount = mResp["products"] as! [Int]
but it is crashed every time when it is not having array.
Now i am not getting how to check this because i have the different option for Int and Array?
Please help me.Thanks
There is no need to fall back to Any here. Even problematic JSON like this can be handled with Codable. You just need to keep trying the different types until one works.
struct Thing: Decodable {
let products: [Int]
enum CodingKeys: String, CodingKey {
case products
}
init(from decoder: Decoder) throws {
// First pull out the "products" key
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
// Then try to decode the value as an array
products = try container.decode([Int].self, forKey: .products)
} catch {
// If that didn't work, try to decode it as a single value
products = [try container.decode(Int.self, forKey: .products)]
}
}
}
let singleJSON = Data("""
{ "products": 25 }
""".utf8)
let listJSON = Data("""
{ "products": [77,80,81,86] }
""".utf8)
let decoder = JSONDecoder()
try! decoder.decode(Thing.self, from: singleJSON).products // [25]
try! decoder.decode(Thing.self, from: listJSON).products // [77, 80, 81, 86]
It crashes because you force unwrap as an Integer Array, even though you just have an integer. The solution is to check for both:
self.productsCount = mResp["products"] as? [Int] ?? mResp["products"] as? Int
Other Solution
if let proCount = mResp["products"] as? [Int] {
self.productsCount = proCount
} else {
self.productsCount = mResp["products"] as? Int
}
This is temporary solution as you want. Check for possible type with "Any" type.
var anyType : Any!
anyType = "123"
anyType = ["Test","Test1"]
anyType = 1
if anyType is Array {
print("is Array")
}else if anyType is String {
print("is String")
}else if anyType is Int {
print("is Int")
}
let dict = [77,80,81,86]//Pass your parameter or parsed json value
if dict is Array<Any> {
print("Yes, it's an Array")
}
else{
print("NO, it's not an Array")
}
let's assume your json name is jsonData
Check for Int and Array Int:
if let intVal = jsonData["products"] as? Int {
print("Products is a Integer: ", intVal)
} else if let jsonArr = jsonData["products"] as? [Int] {
var intVals = [Int]()
for json in jsonArr {
intVals.append(json)
}
print("Json is array of Int: ", intVals)
}
Generic solution would be like this,
let products = mResp["products"] as? Any
if let item = products as? [Int] {
print("array", item)
} else if let item = products as? Int {
print("Integer", item)
}
Use Generics for obtaining a better solution and provide the type at the time of decoding this model.
struct Product<T: Codable>: Codable {
let products: T?
}
And you can use it with nested try catch:
do {
let product = try JSONDecoder().decode(Product<Int>.self, from: data)
print(product)
} catch {
do {
let product = try JSONDecoder().decode(Product<[Int]>.self, from: data)
print(product)
} catch {
print(error)
}
}
Note: This solution assumes there is not more than a few different type-varying properties in the codable struct. If there are multiple type-varying properties I'd recommend use a custom init(decoder:) as provided in the accepted answer which would be much better design instead of having a try-catch tree.

Dynamically change API data fetching structure

I am fetching data from an API that keeps changing it's format several times a day from String to Double erratically..
Is it possible to do something to the struct to prevent returning nil when fetching and automatically using with the right type?
struct BitcoinGBP : Decodable {
let price : Double
let percentChange24h : Double
private enum CodingKeys : String, CodingKey {
case price = "PRICE"
case percentChange24h = "CHANGEPCT24HOUR"
}
}
Would simply using Double? work?
Write a custom initializer to handle both cases
struct BitcoinGBP : Decodable {
let price : Double
let percentChange24h : Double
private enum CodingKeys : String, CodingKey {
case price = "PRICE"
case percentChange24h = "CHANGEPCT24HOUR"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
percentChange24h = try container.decode(Double.self, forKey: .percentChange24h)
do {
price = try container.decode(Double.self, forKey: .price)
} catch DecodingError.typeMismatch(_, _) {
let stringValue = try container.decode(String.self, forKey: .price)
price = Double(stringValue)!
}
}
}
In my experience you need to design your structures, models to correspond to what you expect to use in your application and not what API will give you. You need to be prepared for the API to give you incorrect data types or missing, strange data.
If API changes too much then users will need to update but this is generally never done. Endpoints must have versions and need to change version when a big difference is made. If this is not the case that it is flawed API (these things still do happen) in which case you should use your own server, proxy which connects to desired server and modifies their API to your own. You put some tests on it to detect if their API has changed and modify your own as quickly as possible so all applications can still connect to it. But this is way out of the scope of this question.
If your structure must be with having price and percentage change in 24h where both are double then simply expect string or anything from the API. I like to avoid any tools that do that automatically so I just use convenience constructors and a set of tools of my own to produce something like:
class BitcoinGBP {
var price: Double = 0.0
var priceChange24h: Double = 0.0
convenience init(price: Double, priceChange24h: Double) {
self.init()
self.price = price
self.priceChange24h = priceChange24h
}
/// Construuctor designed to accept JSON formated dictionary
///
/// - Parameter descriptor: A JSON descriptor
convenience init(descriptor: [String: Any]) {
self.init()
self.price = double(value: descriptor["PRICE"]) ?? 0.0
self.priceChange24h = double(value: descriptor["CHANGEPCT24HOUR"]) ?? 0.0
}
/// A conveninece method to extract a double value
///
/// - Parameter value: A value to be converted
/// - Returns: Double value if possible
func double(value: Any?) -> Double? {
if let value = value as? String {
return Double(value)
} else if let value = value as? Double {
return value
} else if let value = value as? Float {
return Double(value)
} else if let value = value as? Int {
return Double(value)
} else if let value = value as? Bool {
return value ? 1.0 : 0.0
} else {
return nil
}
}
}
Now the rest is just depending on what functionality you need.

Resources