I have been working on this bug for about 8 hours now.
Into the function mapJsonToRest I am passing in a JSON object (I have verified it is working inside the function up until the guard let rest statement) and a String. From what I understand the String is trivial, currently "id" is being passed into the function.
From here things gets wonky. This statement is returning out of the function ( I am guessing because it is null.)
guard let rest = object[restKey] as? [[String: AnyObject]] else { return mappedRest }
However I have no idea why it would be doing this. It seems that restKey is null which is causing the else to return. I would appreciate any help because my poor head doesn't have much hair left to pull out.
If it helps, the tutorial I have been following was done in Swift 3 and I am writing this in Swift 5.
import Foundation
class RestDataParser {
static func mapJsonToRest(object: [String: AnyObject], restKey: String) -> [Restaurant] {
var mappedRest: [Restaurant] = []
//This is the humdinger right here
guard let rest = object[restKey] as? [[String: AnyObject]] else { return mappedRest }
for resting in rest {
guard let id = resting["id"] as? String,
let name = resting["name"] as? String,
let rating = resting["rating"] as? String,
let location = resting["address1"] as? String,
let imageURL = resting["image_url"] as? String else { continue }
print("By George, I think we've got it.")
let restClass = Restaurant(id: id, name: name, rating: rating, location: location, imageURL: imageURL)
mappedRest.append(restClass)
}
return mappedRest
}
}
Related
I have a string (snippet below) that I know is valid json by parsing it here. The final parsing that I want to achieve is based off this PHP code. I can not seem to keep xcode happy when I try the following code, as I seem to continue to make array's of <Any> and either get the error Value of type 'Any' has no subscripts or an unwrapping of a nil
any thoughts how to parse this would be greatly appreciated.
if let jsonArray = try JSONSerialization.jsonObject(with: jsonString.data(using: .utf8)!,options : .allowFragments) as? Array<Any> {
let a = jsonArray[0] as? Array<Any>
let a = jsonArray[0] as? Array<Any>
let b = a?[1] as? Array<Any>
let c = b?[1] as? Array<Array<Any>>
}
Snippet - full string here
[["comic books",[["","","","","","","","","Ba6eX5HWBc3btAa6mJ_4Cg","0ahUKEwjR7NSWsOHsAhXNLc0KHTrMB68QmBkIAigA",["comic books",0]
]
,["","","","","","","","","Ba6eX5HWBc3btAa6mJ_4Cg","0ahUKEwjR7NSWsOHsAhXNLc0KHTrMB68QmBkIBCgB","","","","",["Ba6eX5HWBc3btAa6mJ_4Cg","0ahUKEwjR7NSWsOHsAhXNLc0KHTrMB68Q8BcIBSgAMAA",["330 Army Trail Rd","Glendale Heights, IL 60139"]
,"",["","","",["https://www.google.com/search?q\u003dGot+Comics+Inc,+330+Army+Trail+Rd,+Glendale+Heights,+IL+60139\u0026ludocid\u003d7330282631276370350#lrd\u003d0x880fac8c25bbd32b:0x65ba6537347199ae,1","105
First, let's declare the data type:
struct Item {
let name: String?
let location: String?
let phone: String?
}
Using SwiftyJSON, you can nicely convert the PHP code into something like this:
var items = [Item]()
if let json = try? JSON(data: jsonData), let arr = json[0][1].array {
for itemArray in arr {
guard let itemDetails = itemArray[14].array else { continue }
let name = itemDetails[11].string
let location = itemDetails[18].string
let phone = itemDetails[178][0][0].string
items.append(Item(name: name, location: location, phone: phone))
}
}
For fun, here's a shorter, more functional, less readable, implementation of the above code:
let items = (try? JSON(data: jsonData))?[0][1].array?
.compactMap { $0[14].array }
.map { Item(name: $0[11].string, location: $0[18].string, phone: $0[178][0][0].string) }
?? []
I'm trying to access the first result from this query:
https://www.instagram.com/web/search/topsearch/?query=_myUsername
I'm able to get a JSON object like so:
var request = URLRequest(url: URL(string: api)!)
request.httpMethod = "GET"
let task = URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { // check for fundamental networking error
print("error=\(error ?? "" as! Error)")
return
}
do {
let jsonResponse = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
completionHandler(jsonResponse,nil)
} catch let parsingError {
print("Error", parsingError)
}
if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 { // check for http errors
print("statusCode should be 200, but is \(httpStatus.statusCode)")
print("response = \(String(describing: response))")
}
}
task.resume()
The result is a JSON object that omits the first user in "users". For example, if I parse the JSON object to get the username of the first user in the result like this...
if let users = jsonResponse!["users"] as? [Any] {
if let first = users.first as? [String: Any] {
if let user = first["user"] as? [String: Any] {
self.igUser = user["username"] as! String
... It returns the username of the 'position = 1' user, while I actually want the 'position = 0' user. Am I parsing this wrong?
As you can see there is a key position you should assume that the list isn't sorted. You have to find the nth element of the list.
The minimal Codable implementation would be:
struct TopSearchAPIResponse: Codable {
let users: [User]
//let places, hashtags: [Type] // As these two are empty arrays you don't know
// their type in advance. So you can omit them
// for now. When you know their type you can
// use them by providing actual type.
let hasMore: Bool
let rankToken: String
let clearClientCache: Bool
let status: String
struct User: Codable {
let position: Int
let user: UserInfo
struct UserInfo: Codable {
let pk: String
let username: String
let fullName: String
let isPrivate: Bool
let profilePicURL: URL
let profilePicID: String?
let isVerified: Bool
let hasAnonymousProfilePicture: Bool
let followerCount: Int
let reelAutoArchive: ReelAutoArchive
let byline: String
let mutualFollowersCount: Int
let unseenCount: Int
private enum CodingKeys: String, CodingKey {
/* This enum is necessary as we want profile_pic_url & profile_pic_id
to be decoded as profilePicURL & profilePicID respectively (instead of
profilePicUrl & profilePicId) so that we follow Swift conventions */
case pk
case username
case fullName
case isPrivate
case profilePicURL = "profilePicUrl"
case profilePicID = "profilePicId"
case isVerified
case hasAnonymousProfilePicture
case followerCount
case reelAutoArchive
case byline
case mutualFollowersCount
case unseenCount
}
enum ReelAutoArchive: String, Codable {
case off
case on
case unset
}
}
}
}
You will use it as:
do {
let jsonDecoder = JSONDecoder()
jsonDecoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try jsonDecoder.decode(TopSearchAPIResponse.self, from: data)
if let firstUser = response.users.first(where: { $0.position == 0 }) {
print(firstUser.user.username) // prints "myusernameisverygay"
}
} catch {
print(error)
}
Note: Some modifications had been made after the answer was accepted.
I have the following swift3 code. The JSON can return a NSNull value for the $0[2] value.
struct Player3 {
let name : String
var score : String
let avatar : String
}
class HistoricLeagueVC: UITableViewController {
var players = [Player3]()
override func viewDidLoad() {
super.viewDidLoad()
let urlString = "https://str8red.com/jsonoverallleaderboard/1025/"
let url = URL(string: urlString)
URLSession.shared.dataTask(with:url!) { (data, response, error) in
if error != nil {
print("there was an error")
} else {
do {
let parsedData = try JSONSerialization.jsonObject(with: data!) as! [[String]]
self.players = parsedData.map { Player3(name: $0[0], score: $0[1], avatar: $0[2]) }
print(self.players.count)
DispatchQueue.main.async {
self.tableView.reloadData()
}
} catch let error as NSError {
print(error)
}
}
}.resume()
}
I have tried to convert it to an empty string without success. I have also tried to set a default string such as "https://str8red.com/static/no_profile_picture.png" which ideally is what I would like to do.
The error in the terminal states Could not cast value of type 'NSNull' (0xed1c78) to 'NSString' (0x57b6b8).
Any help would be appreciated.
Do not cast to [[String]] then:
let parsedData = try JSONSerialization.jsonObject(with: data!) as! [[Any]]
self.players = parsedData.map { Player3(
name: ($0[0] as? String) ?? "",
score: ($0[1] as? String) ?? "",
avatar: $0[2] as? String, // keeping `nil` here
)}
You should probably even as! [[Any]] do in a safer manner (using as?) but the above will work.
Your code will crash brutally if anything goes wrong with your input data. Ask your boss if he or she is Ok with that. I wouldn't. Here's how you will crash: If data == nil (but it shouldn't be if error == nil). You catch the problem that data cannot be parsed without crashing. You crash if data doesn't parse to an array, you crash if the elements of the array are not all arrays, you crash if the elements inside the inner arrays are not all Strings, and you crash if any of the inner arrays contain two or fewer strings. I'd say that is pretty unacceptable.
My strategy would be: If the response cannot be parsed or is not an array of arrays, then ignore it, leaving self.players unchanged. If there is an array of arrays, reset self.players, then add a player for each array that contains at least three items, of which the first two must be strings.
guard data != nil else { return }
guard let parsed = try? NSJSONSerialization(with: data!) else { return }
guard let arrays = parsed as? [[Any]] else { return }
self.players.removeAll()
for array in arrays {
guard array.count >= 3,
let name = array [0] as? String,
let score = array [1] as? String else { continue }
let avatar = array [2] as? String ?? ""
self.players.append(Player3 (name: name, score: score, avatar: avatar))
}
It's a good idea to move something like this into a separate method. Add whatever error handling or logging you feel is appropriate.
I have inherited a Swift project from another developer. We are adding an imageUrl that can now be a part of an item which
is returned from a JSON api. We are returning the using Alamofire.
One issue is that we can have a null value for our instore_image_url_200w which
becomes the item's imageUrl. I guess I have two questions. It would seem like I'd like to have imageUrl be an optional string. Is this correct? If yes, what
is the proper way to handle the nil value during initialization? I'd like to be checking for nil on display for item.imageUrl
var items = [Item]()
Alamofire.request(URL(string: "https://www.example.com/api/v1/metro_areas/1")!, method: .get).validate().responseJSON { (response) -> Void in
guard let value = response.result.value as? [String: Any],
let rows = value["items"] as? [[String: Any]] else {
print("Malformed data received from fetchAllRooms service")
return
}
for row in rows {
let item = Item(data: row)
if (item.imageUrl is String){
print("here is item.imageUrl: \(item.imageUrl!)")
}
items.append(item)
}
}
and the model:
class Item {
var name: String
var imageUrl: String?
init(name: String, imageUrl: String) {
self.name = name
self.imageUrl = imageUrl
}
convenience init(data: [String:Any]) {
let name = data["header"] as? String
let imageUrl = data["instore_image_url_200w"] as! String?
self.init(name: name!,
imageUrl: imageUrl)
}
}
Yes, the imageUrl would be an optional string since it might, or might not, exist in the JSON data. One point - you currently have your model initializer code for the imageUrl as:
let imageUrl = data["instore_image_url_200w"] as! String?
Personally, I think it would be cleaner to have it as:
let imageUrl = data["instore_image_url_200w"] as? String
As for your second question, if you have the initialization as above, the nil value would be automatically handled during initialization since the imageUrl value is optional and so it would either be set to nil (on a nil value) or have an actual string value.
Of course, when you use imageUrl you'd always have to do a check to see if it is nil or has a value, but you already mentioned that you'd be doing that on display.
I make some kind of JSON parsing with a weatherAPI. To prevent me of running into nullPointer (maybe the page is offline or something) I covered my parsing into a guard, because I thought he protect me for unwrapping optional value.
guard
let loadedWeather = (json["weather"] as! [[String:AnyObject]])[0]["description"] as? String,
let loadedTemperatur = (json["main"] as! [String:AnyObject])["temp"] as? Float,
let loadedWindSpeed = (json["wind"] as! [String:AnyObject])["speed"] as? Float
else {
print("Weather JSON-Parsing failed")
return
}
Have I misunderstood the function of guard? If so, how can I solve that problem?
Actually the page is offline or something and returns an error JSON page with other tags, so "weather", "main"...etc. not include. I hoped in that case my guard brings me inside my else statement so I can handle with that case.
Thanks a lot.
You are misusing the syntax for guard - use it as follows:
guard let weatherArray = json["weather"] as? [[String:AnyObject]], let mainDict = json["main"] as? [String:AnyObject], let windDict = json["wind"] as? [String:AnyObject] else {
return
}
guard let dict = weatherArray.first else {
return
}
guard let description = dict["description"] as? String, let temp = mainDict["temp"] as? Float, let speed = windDict["speed"] as? Float else {
return
}
Though I would recommend you follow the if let syntax as outlined by Damien.
You should look for conditional unwrapping : https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html
if let weather = json["weather"] {
// weather is define here, you can use it
}
Change
as! [[String:AnyObject]]
to
as? [[String:AnyObject]]
You are trying to force cast your json data (as!) so even though you are using guard you will still get a crash at that spot if its nil.
Edit: You said its still forcing as! so maybe try to split up your code like so. Should also make it more readable and easier for you to get other info out of the dicts/arrays of the json response. Something like this should work
/// Get json data
guard
let loadedWeather = json["weather"] as? [[String:AnyObject]],
let loadedTemperatur = json["main"] as? [String:AnyObject],
let loadedWindSpeed = json["wind"] as? [String:AnyObject]
else {
print("Weather JSON-Parsing failed")
return
}
/// Get info from json data
guard
let weatherDescription = loadedWeather[0]["description"] as? String,
let temperature = loadedTemperatur["temp"] as? Float,
let windSpeed = loadedWindSpeed["speed"] as? Float
else {
print("Weather JSON-Parsing failed")
return
}
/// do something with weather description, temperature, windSpeed
Maybe even better try to split up those guard statements for each line separately so incase one fails your whole block doesn't exit. In that case better to use if let because you dont want to exit early. Just dont start any pyramids of doom with if let statements.
/// Weather
if let loadedWeather = json["weather"] as? [[String:AnyObject]],
let weatherDescription = loadedWeather[0]["description"] as? String {
// do something with weather description
}
/// Temperature
if let loadedTemperatur = json["main"] as? [String:AnyObject],
let temperature = loadedTemperatur["temp"] as? Float {
// do something with temperature
}
/// Wind speed
if let loadedWindSpeed = json["wind"] as? [String:AnyObject],
let windSpeed = loadedWindSpeed["speed"] as? Float {
// do something with windspeed
}
Hope this helps.