Converting a JSON into a Model Value in Swift - ios

In my quest to learn more about Swift, I'm looking at ways to improve my app and noticed a few places where I'm making assumptions where perhaps I shouldn't be.
When creating a new object, lets say a 'student', they need things like a name (String), age (Int) and score (Float). I read these from a JSON file, and put them into an object like this:
// note, details is a [String:Any] type
let name = details["name"] as! String
let age = details["age"] as! Int
let score = Float(details["score"])
self.student = Student(name: name, tutor_group: tutor_group, score: score)
So my questions are as follows;
1. How should I modify my code to check that if a value is not a number, where it should be, the variable becomes just nil, or even better 0?
2. What if the key in the dictionary doesn't exist?
3. Are the different ways to do this, and if so, which is best practice?
Note that I want to keep this code as short as possible - if/else statements for each line are not what I'm looking for.
Thank you so much in advance!

The Solution suggested by the Swift team
Recently Apple described the suggested way to face this problem.
You can define the Student struct (or class) this way
struct Student {
let name: String
let age: Int
let score: Float
init?(json: [String:Any]) {
guard
let name = json["name"] as? String,
let age = json["age"] as? Int,
let score = json["score"] as? Float
else { return nil }
self.name = name
self.age = age
self.score = score
}
}
Benefits of this approach
You encapsulate the logic to convert a JSON into a Student inside the Student struct itself
If the JSON doesn't contain a valid data (e.g. there is no age field with a valid Int) then the initializer fails and returns nil. This means there is no way to create a Student with missing fields (and this is a good thing) and this scenario will not cause a crash.
More
The post I linked above also describes a more advanced approach where a missing field/value throws an exception. However if you don't need to log (or notify to the caller) why the initialization failed you don't need this.

So my questions are as follows; 1. How should I modify my code to check that if a value is not a number, where it should be, the variable becomes just nil, or even better 0? 2. What if the key in the dictionary doesn't exist? 3. Are the different ways to do this, and if so, which is best practice?
let age = (details["age"] as? Int) ?? 0
In all cases, age will have the type Int
If the key doesn't exist, details["age"] will return nil, as? Int will return an Int? with value nil and the nil coalescing operator ?? will set the value to 0.
If the type isn't an Int, the conditional cast as? Int will return nil and the value will be set to 0.
In the expected case, age will have the Int value that was stored in details["age"].
For the other fields:
let name = (details["name"] as? String) ?? ""
// If score is stored as a String
let score = Float(details["score"] as? String ?? "") ?? 0
OR
// If score is stored as a number
let score = (details["score"] as? Float) ?? 0

You can use guard instead:
guard let name = details["name"] as? String else {
return
}
print("\(name)")
Thanks!

Related

convert value of type 'String' to expected argument type 'Int'

I have a dictionary of data passed from another viewController, and when I tried to fetch the id from the dictionary it gives me conversion error!
var comments = [JSON]()
#IBAction func AddCommentBTN(_ sender: Any) {
let commentTXT = CommentTXTField.text
let name = self.accountDetails["name"]
let email = self.accountDetails["email"]
let articleId = self.comments["id"].string! ----> error is here
API.AddComment(articleId: "", name: name!, email: email!, message: commentTXT!) { (error: Error?, success: Bool) in
if success {
self.CommentTableView.reloadData()
print("Registerd Successfuly")
} else {
print("Faile To Comment")
}
}
}
Firstly, self.comments is an array of JSON object. So before getting value from the JSON you need to get the object first from the array. Before getting the 0 index value make sure the array contains value.
Seems like you are using SwiftyJSON to parse the value. So by using .intValue you can get the Int value. .intValue will return a Int type value. If the key not present there then this will return you the 0 value. .intValue not an Optional type.
Try this
let articleId = self.comments[0]["id"].intValue
instead of
let articleId = self.comments["id"].string!
You are converting a String to Int which is not as straightforward as you may think. The String can contain characters that is not a number like "Forest". To convert a String to Int you can use:
if let myInt = Int(self.comments["id"]) {
let articleId = myInt
}
It will check if the string is a correct Int and return an Optional value. You can also use a fallback to define a default value if the String is not correct Int
let myInt = Int(self.comments["id"]) ?? 0
That means “attempt to convert self.comments["id"] to an integer, but if the conversion failed because it contained something invalid then use 0 instead.”
The values of a json are optional and might be on any allowed type. A way parsing this save would be getting the value for "id", which will be an optional, and try to convert this optional to an Int. The Int itself is optional as well. (Short, it might be there or not)
guard let value = self.comments["id"], let articleId = Int64(value) else {
// Not valid
return
}
print(articleId)
A better way would be parsing your json data to an object in the first place. Your controller would not need to deal with the parsing of optional data, but instead just get the right data from the object.

Swift 3 optional string to int

I am using Vapor for Swift backend. Following is the code i am working with.
drop.post("postTodo") { request in
var jsonContent: JSON?
if let contentType = request.headers["Content-Type"], contentType.contains("application/json"), let jsonData = request.json {
jsonContent = jsonData
print("Got JSON: \(jsonContent)")
}
guard let id = jsonContent?.node.object?["id"]?.string
else {
return JSON(["message": "Please include mandatory parameters"])
}
let tempId = Int(id)!
I am getting "id" as optional string for eg: Optional("123") for jsonContent?.node.object?["id"]?.string
When I try to convert it to int using Int(id)! i get back nil
If i try to do let tempId = Int(id!) it gives error.
But when i do the same thing in playground i get proper int value.
let id: String?
id = "1234"
let myInt = Int(id!)
Why Optional string to Int is not working properly in my Vapor app ?
Any idea.
If "id" is an optional string, then you probably don't want to be force unwrapping it with the "!".
The safest approach would be something like:
if let id = id
{
let myIdAsInt = Int(id)
}
The reason it "works" in the playground, is you are definitely assigning a non-nil value to the string (therefore you get away with the force unwrap).
String!might contain a string, or it might contain nil. It’s like a regular optional, but Swift lets you access the value directly without the unwrapping safety. If you try to do it, it means you know there’s a value there – but if you’re wrong your app will crash.
var optionalString: String? = "123"
// first check if it doesn't contain nil
if let str = optionalString {
// string to -> Int
if let id = Int(str) {
print(id) // work with id
}
} else {
// optionalString contains nil
}
what i found is in my iOS code i had a struct with optional properties coz of which when mapped to Dict gave object with optional values to keys.
If I make properties non optional and send it to vapor backend after it works fine.
So basically it was the case of using Optionals properly.

iOS Swift: Could not cast value type '__NSCFNumber' to 'NSString'

I am retrieving a number value from my Firebase database (JSON db) and then displaying this number into a textField, although I get this error when I try to display it.
Could not cast value type '__NSCFNumber' to 'NSString'
How can I properly convert the retrieved value to a String, taking into consideration that this value maybe change between a String and a Number when I retrieve it.
Here is my code:
let quantity = child.childSnapshot(forPath: "quantity").value // Get value from Firebase
// Check if the quantity exists, then add to object as string.
if (!(quantity is NSNull) && ((quantity as! String) != "")) {
newDetail.setQuantity(quantity: quantity as! String)
}
The error is saying that your quantity is Number and you cannot directly convert number to String, try like this.
newDetail.setQuantity(quantity: "\(quantity)")
Or
if let quantity = child.childSnapshot(forPath: "quantity").value as? NSNumber {
newDetail.setQuantity(quantity: quantity.stringValue)
}
else if let quantity = child.childSnapshot(forPath: "quantity").value as? String {
newDetail.setQuantity(quantity: quantity)
}
Or With Single if statement
if let quantity = child.childSnapshot(forPath: "quantity").value,
(num is NSNumber || num is String) {
newDetail.setQuantity(quantity: "\(quantity))
}
Using second and third option there is no need to check for nil.
Swift 4:
let rollNumber:String = String(format: "%#", rollNumberWhichIsANumber as! CVarArg)
You may convert your NSNumber value to string like this too, its more traditional and basic approach to format a string
newDetail.setQuantity(String(format: "%#", quantity))
Swift 4.
newDetail.setQuantity(String(describing: rollNumberWhichIsANumber))
In swift 4.1 and Xcode 9.4.1
newDetail.setQuantity(quantity: "(quantity)")
We can convert to string like this "\(quantity)"
let session_id : Int32 = (jsonObject.value(forKey: "id") as! NSNumber).int32Value
Swift 5:
I manage to fix it buy using .description when interpolating the UserDefaults value into a label converting from Int to String.
working code
highScoreLabel?.text = "HiScore: \(hiScoreValue.description)"
old code
highScoreLabel?.text = "HiScore: \(String(describing: hiScoreValue))"

SWIFT access nested dictionary

I have a Dictionary that has a User object, and that User object is a dictionary that has a key "Name".
In Swift, i need to access the value for "Name".
So I did the following:
let user = question[kUserOwner] as! PFUser
let userName = user[kName] as! String
userButton.setTitle(userName, forState:UIControlState.Normal)
1) Is there really no easier/shorter way to do this?
In Objective C:
[_userButton setTitle:[[question objectForKey:kUserOwner] objectForKey:kName] forState:UIControlStateNormal];
I do realize that it is not Type safe but I can live with that, as long as I know what I am doing.
2) Is there any way i can avoid casting?
When you subscript, you get an Optional. And you cannot subscript an Optional. Therefore, while you can perhaps avoid casting, you cannot avoid unwrapping:
let dinner = ["name":"Matt"]
let douter = ["owner":dinner]
let name = douter["owner"]!["name"]
But that only works because Swift knows very specifically what douter is. It would be better, therefore, to do this in stages, as Swift expects you to do, e.g. with a nested series of if let bindings:
let dinner : AnyObject = ["name":"Matt"] as AnyObject
let douter : AnyObject = ["owner":dinner] as AnyObject
if let owner = douter["owner"] as? [NSObject:AnyObject],
let name = dinner["name"] as? String {
// do something with name
}

Swift optionals: language issue, or doing something wrong?

I am doing what I believe to be a very simple task. I'm trying to get a value out of a dictionary if the key exists. I am doing this for a couple keys in the dictionary and then creating an object if they all exist (basically decoding a JSON object). I am new to the language but this seems to me like it should work, yet doesn't:
class func fromDict(d: [String : AnyObject]!) -> Todo? {
let title = d["title"]? as? String
// etc...
}
It gives me the error: Operand of postfix ? should have optional type; type is (String, AnyObject)
HOWEVER, if I do this, it works:
class func fromDict(d: [String : AnyObject]!) -> Todo? {
let maybeTitle = d["title"]?
let title = maybeTitle as? String
// etc...
}
It appears to be basic substitution but I may be missing some nuance of the language. Could anyone shed some light on this?
The recommended pattern is
if let maybeTitle = d["title"] as? String {
// do something with maybeTitle
}
else {
// abort object creation
}
It is possibly really a question of nuance. The form array[subscript]? is ambiguous because it could mean that the whole dictionary (<String:AnyObject>) is optional while you probably mean the result (String). In the above pattern, you leverage the fact that Dictionary is designed to assume that accessing some key results in an optional type.
After experimenting, and noticing that the ? after as is just as ambiguous, more, here is my solution:
var dictionary = ["one":"1", "two":"2"]
// or var dictionary = ["one":1, "two":2]
var message = ""
if let three = dictionary["three"] as Any? {
message = "\(three)"
}
else {
message = "No three available."
}
message // "No three available."
This would work with all non-object Swift objects, including Swift Strings, numbers etc. Thanks to Viktor for reminding me that String is not an object in Swift. +
If you know the type of the values you can substitute Any? with the appropriate optional type, like String?
There are a few of things going on here.
1) The ? in d["title"]? is not correct usage. If you're trying to unwrap d["title"] then use a ! but be careful because this will crash if title is not a valid key in your dictionary. (The ? is used for optional chaining like if you were trying to call a method on an optional variable or access a property. In that case, the access would just do nothing if the optional were nil). It doesn't appear that you're trying to unwrap d["title"] so leave off the ?. A dictionary access always returns an optional value because the key might not exist.
2) If you were to fix that:
let maybeTitle = d["title"] as? String
The error message changes to: error: '(String, AnyObject)' is not convertible to 'String'
The problem here is that a String is not an object. You need to cast to NSString.
let maybeTitle = d["title"] as? NSString
This will result in maybeTitle being an NSString?. If d["title"] doesn't exist or if the type is really NSNumber instead of NSString, then the optional will have a value of nil but the app won't crash.
3) Your statement:
let title = maybeTitle as? String
does not unwrap the optional variable as you would like. The correct form is:
if let title = maybeTitle as? String {
// title is unwrapped and now has type String
}
So putting that all together:
if let title = d["title"] as? NSString {
// If we get here we know "title" is a valid key in the dictionary, and
// we got the type right. title has now been unwrapped and is ready to use
}
title will have the type NSString which is what is stored in the dictionary since it holds objects. You can do most everything with NSString that you can do with String, but if you need title to be a String you can do this:
if var title:String = d["title"] as? NSString {
title += " by Poe"
}
and if your dictionary has NSNumbers as well:
if var age:Int = d["age"] as? NSNumber {
age += 1
}

Resources