Is there a better way to get numbers from dictionary (JSON data)? - swift

Assume I receive data in some JSON which is then parsed to dictionary using a native tool. Some of those values are numbers and naturally I will need to parse them, cast them into what I need in the application.
But the transition from Any to an actual number such as Float, Int, Double seems to be a bit messy. For instance if I expect a double but at some point server returns an integer my code will fail using:
let doubleValue = dictionary["key"] as! Double
So this will work when the item is 1.3 but will fail for 1. To use a bit more concrete example we can use the following:
let myDictionary: [String: Any] = [
"myNumber_int" : 1,
"myNumber_float" : Float(1),
"myNumber_cgFloat" : CGFloat(1),
"myNumber_double" : 1.0
]
let numberInt1 = myDictionary["myNumber_int"] as? Int // 1
let numberInt2 = myDictionary["myNumber_int"] as? Float // nil
let numberInt3 = myDictionary["myNumber_int"] as? CGFloat // nil
let numberInt4 = myDictionary["myNumber_int"] as? Double // nil
let numberFloat1 = myDictionary["myNumber_float"] as? Int // nil
let numberFloat2 = myDictionary["myNumber_float"] as? Float // 1
let numberFloat3 = myDictionary["myNumber_float"] as? CGFloat // nil
let numberFloat4 = myDictionary["myNumber_float"] as? Double // nil
let numberCGFloat1 = myDictionary["myNumber_cgFloat"] as? Int // nil
let numberCGFloat2 = myDictionary["myNumber_cgFloat"] as? Float // nil
let numberCGFloat3 = myDictionary["myNumber_cgFloat"] as? CGFloat // 1
let numberCGFloat4 = myDictionary["myNumber_cgFloat"] as? Double // nil
let numberDouble1 = myDictionary["myNumber_double"] as? Int // nil
let numberDouble2 = myDictionary["myNumber_double"] as? Float // nil
let numberDouble3 = myDictionary["myNumber_double"] as? CGFloat // nil
let numberDouble4 = myDictionary["myNumber_double"] as? Double // 1
So for each type only 1 cast will actually work which is... well I would at least expect that CGFloat will be able to cast directly to Double or Float...
So my solution is using NSNumber:
let myNumbers: [CGFloat] = [
CGFloat((myDictionary["myNumber_int"] as? NSNumber)?.floatValue ?? 0.0),
CGFloat((myDictionary["myNumber_float"] as? NSNumber)?.floatValue ?? 0.0),
CGFloat((myDictionary["myNumber_cgFloat"] as? NSNumber)?.floatValue ?? 0.0),
CGFloat((myDictionary["myNumber_double"] as? NSNumber)?.floatValue ?? 0.0),
] // [1,1,1,1]
This naturally works but the code is pretty ugly for something as simple as this. But putting the code "ugliness" aside; can we not do similar without using Next Step? I mean NSNumber for something as seemingly trivial as this? I am missing something very obvious here, right?

Try like this:-
if let data = myDictionary as? [String: AnyObject] {
let myNumber = data["myNumber_int"] as? Int
}

Related

Need to unwrap Int?

I keep getting this error message: Value of optional type 'Int?' must be unwrapped to a value of type 'Int'
with this code:
let data = document.data()
let uid = data["userid"] as? String ?? ""
let location = data["location"] as? String ?? ""
let currentRating = data["currentRating"] as? Int
let usualRating = data["usualRating"] as? Int
var Submission = RatingSubmission(uid: uid, location: location, currentRating: currentRating, usualRating: usualRating)
what do I need to add to currentRating and usualRating in the Submission variable so that it runs properly?
The reason is that RatingSubmission function works with Int values but you if you assign them as optional values there is a chance to these variables may be "nill". To avoid this you can either remove this option like this:
let currentRating = data["currentRating"] as? Int ?? 0
let usualRating = data["usualRating"] as? Int ?? 0
Second option, if you sure these values are not nill, you can downcasting using "as!".(this is not safe most time)
let currentRating = data["currentRating"] as! Int
let usualRating = data["usualRating"] as! Int
Third option, you can check the values with if let;
let currentRating = data["currentRating"] as? Int
let usualRating = data["usualRating"] as? Int
if let currentRating = Int(data["currentRating"]), let usualRating = Int(data["usualRating"]) {
var Submission = RatingSubmission(uid: uid, location: location, currentRating: currentRating, usualRating: usualRating)
}
I would recommend using the get() method on the document which was made specifically for getting properties from documents.
if let usualRating = document.get("usualRating") as? Int {
print(usualRating)
}
Alternatively, you can one-line it by giving it a default value (of 0 in this case). But this would appear like an anti-pattern to me just on the face of it.
let usualRating = document.get("usualRating") as? Int ?? 0

JSON parsing float in swift4.1 has unexpected behaviour

A simple function that parse json variable and returns a float.
func parseMyFloat(jsonString: String) -> Float? {
if let data = jsonString.data(using: String.Encoding.utf8, allowLossyConversion: true),
let parsedJSON = try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments) as? [String : Any] {
if let parsedJSON = parsedJSON {
return parsedJSON["myFloat"] as? Float
}
}
return nil
}
Now if i try this.
print(parseMyFloat(jsonString: "{\"myFloat\":23.2322998046875}"))
// output: 23.2322998
output is fine but if I change 23.2322998046875 value to 23.2322998046 func returns nil.
print(parseMyFloat(jsonString: "{\"myFloat\":23.2322998}"))
// output: nil
Then I tried casting Any to Float which doesn't work.
let dic:[String : Any] = ["float1" : 23.2322998046875, "float2" : 23.2322998]
print(dic["float1"] as? Float) // prints nil
print(dic["float2"] as? Float) // prints nil
I have lots of float in my code, so after migrating to swift 4.1 I am having this issue.
Should I change all Float's to Double's ?? And 23.2322998046875 why works and why not 23.2322998??
If you want to keep your function as it is and don't want to modify the return value to Double, you can simply parse the JSON value as Double to avoid the issue of casting Any to Float as explained in the comments and then convert the Double to Float before returning it.
func parseMyFloat(jsonString: String) -> Float? {
if let data = jsonString.data(using: String.Encoding.utf8, allowLossyConversion: true), let parsedJSON = (try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)) as? [String : Any], let myDouble = parsedJSON["myFloat"] as? Double {
return Float(myDouble)
}
return nil
}
parseMyFloat(jsonString: "{\"myFloat\":23.2322998046875}") //23.2323998
parseMyFloat(jsonString: "{\"myFloat\":23.2322998}") //23.2323998

Return from initializer without initializing all stored properties error - yet everything is initialized

I'm running into this weird error. it was working fine until I added the image and imageString values and then this error happened:
Return from initializer without initializing all stored properties
I thought I initialized all the properties, not sure why this error is happening. Here is the custom object class
class JSONObject {
private let baseImageURL = "https://website.com"
var airbnbUS: Int
var airbnbLocal: Int
var imageString: String
var image: URL
init(airbnbUS: Int, airbnbLocal: Int, imageString: String, image: URL ){
self.airbnbUS = airbnbUS
self.airbnbLocal = airbnbLocal
self.imageString = imageString
self.image = image
}
init(resultsDictionary:[String: Any]){
guard let cost = resultsDictionary["cost"] as? [String: Any],
let airbnb = cost["airbnb_median"] as? [String : Any],
let usd = airbnb["USD"] as? Int,
let chf = airbnb["CHF"] as? Int
else {
airbnbUS = 0
airbnbLocal = 0
return
}
airbnbUS = usd
airbnbLocal = chf
guard let media = (resultsDictionary["media"] as? [String: Any]),
let imageDictionary = media["image"] as? [String: Any],
let image1000 = imageDictionary["1000"] as? String
else {
imageString = ""
image = URL(string: "\(baseImageURL)")!
return
}
imageString = image1000
image = URL(string: "\(baseImageURL)\(imageString)")!
}
}
The issue is in your resultsDictionary initializer. The return in your first guard statement could return from the initializer early, and the following guard statement (and the code where you assign the image and imageString properties) might not execute.
One solution is to change the first guard statement to an if-let statement.
init(resultsDictionary:[String: Any]){
if let cost = resultsDictionary["cost"] as? [String: Any],
let airbnb = cost["airbnb_median"] as? [String : Any],
let usd = airbnb["USD"] as? Int,
let chf = airbnb["CHF"] as? Int
{
airbnbUS = usd
airbnbLocal = chf
} else {
airbnbUS = 0
airbnbLocal = 0
}
guard let media = (resultsDictionary["media"] as? [String: Any]),
let imageDictionary = media["image"] as? [String: Any],
let image1000 = imageDictionary["1000"] as? String
else {
imageString = ""
image = URL(string: "\(baseImageURL)")!
return
}
imageString = image1000
image = URL(string: "\(baseImageURL)\(imageString)")!
}

possible to cast this Alamofire result to an array of dictionaries

I am not an iOS dev and have to make a few changes to a Swift / AlamoFire project (not mine) and am a bit lost.
I have the following JSON:
{"metro_locations":
[
{
"name":"Ruby Red"
},
{
"name":"Blue Ocean"
}
]
}
class (I know that there are issues here):
class Location{
var name=""
init(obj:tmp){
self.name=tmp["name"]
}
}
and need to make an AlamoFire call
Alamofire.request(.GET, "https://www.domain.com/arc/v1/api/metro_areas/1", parameters: nil)
.responseJSON { response in
if let dataFromNetworking = response.result.value {
let metroLocations = dataFromNetworking["metro_locations"]
var locations: [Location]=[]
for tmp in metroLocations as! [Dictionary] { // <- not working, Generic Paramter 'Key' could not be inferred
let location=Location.init(obj: tmp)
locations.append(location)
}
}
}
I have included the error msg, the "not working" but feel that there are issues in other parts too (like expecting a dictionary in the initialization). What does the 'Key' could not be inferred mean and are there other changes I need to make?
edit #1
I have updated my Location to this to reflect your suggestion:
init?(dictionary: [String: AnyObject]) {
guard let id = dictionary["id"] else { return nil }
guard let name = dictionary["name"] else { return nil }
guard let latitude = dictionary["latitude"] else { return nil }
guard let longitude = dictionary["longitude"] else { return nil }
self.name = name as! String
self.id = id as! Int
self.latitude = latitude as! Double
self.longitude = longitude as! Double
}
but I get the error:
Could not cast value of type 'NSNull' (0x10f387600) to 'NSNumber' (0x10f77f2a0).
like this:
I would think that the guard statement would prevent this. What am I missing?
You can cast metroLocations as an array of dictionaries, namely:
Array<Dictionary<String, String>>
Or, more concisely:
[[String: String]]
Thus:
if let dataFromNetworking = response.result.value {
guard let metroLocations = dataFromNetworking["metro_locations"] as? [[String: String]] else {
print("this was not an array of dictionaries where the values were all strings")
return
}
var locations = [Location]()
for dictionary in metroLocations {
if let location = Location(dictionary: dictionary) {
locations.append(location)
}
}
}
Where
class Location {
let name: String
init?(dictionary: [String: String]) {
guard let name = dictionary["name"] else { return nil }
self.name = name
}
}
Clearly, I used [[String: String]] to represent an array of dictionaries where the values were all strings, as in your example. If the values included objects other than strings (numbers, booleans, etc.), then you might use [[String: AnyObject]].
In your revision, you show us a more complete Location implementation. You should avoid as! forced casting, and instead us as? in the guard statements:
class Location {
let id: Int
let name: String
let latitude: Double
let longitude: Double
init?(dictionary: [String: AnyObject]) {
guard let id = dictionary["id"] as? Int,
let name = dictionary["name"] as? String,
let latitude = dictionary["latitude"] as? Double,
let longitude = dictionary["longitude"] as? Double else {
return nil
}
self.name = name
self.id = id
self.latitude = latitude
self.longitude = longitude
}
}

Does not have a member named 'subscript'

I'm building something and it all worked fine until Swift 1.2 came out. I made some changes but still have one line of code that is playing nice. I don't understand why this is breaking:
let swiftArray = positionDictionary.objectForKey["positions"] as? [AnyObject]
it gives me an error:
'(AnyObject) -> AnyObject?' does not have a member named 'subscript'
I also tried using this:
let swiftArray = positionDictionary.objectForKey?["positions"] as? [AnyObject]
but then I get an error saying:
Operand of postfix '?' should have an optional type; type is '(AnyObject) -> AnyObject?'
I'm really confused...can anyone help?
func addOrbsToForeground() {
let orbPlistPath = NSBundle.mainBundle().pathForResource("orbs", ofType: "plist")
let orbDataDictionary : NSDictionary? = NSDictionary(contentsOfFile: orbPlistPath!)
if let positionDictionary = orbDataDictionary {
let swiftArray = positionDictionary.objectForKey["positions"] as? [AnyObject]
let downcastedArray = swiftArray as? [NSArray]
for position in downcastedArray {
let orbNode = Orb(textureAtlas: textureAtlas)
let x = position.objectForKey("x") as CGFloat
let y = position.objectForKey("y") as CGFloat
orbNode.position = CGPointMake(x,y)
foregroundNode!.addChild(orbNode)
}
}
positionDictionary is an NSDictionary. You can use it just like a Swift dictionary - you don't need to use objectForKey.
You should just use if let and optional casting to get the value you want, which I think is an array of NSDictionary since you're using objectForKey again later:
if let downcastedArray = positionDictionary["positions"] as? [NSDictionary] {
for position in downcastedArray {
let orbNode = Orb(textureAtlas: textureAtlas)
let x = position["x"] as CGFloat
let y = position["y"] as CGFloat
orbNode.position = CGPointMake(x,y)
foregroundNode!.addChild(orbNode)
}
}
As a side note, CGPointMake is not stylistically preferred in Swift. Instead, consider using the CGPoint initializer:
orbNode.position = CGPoint(x: x, y: y)