how to map JSON object with MapKit using swift - json

I am working with a MapKit example using swift. The JSON object they are using is a object of arrays. I have a JSON file that is a array of objects. from the example I see they are pulling the properties they want by there location in the array. I need to use the property keys in my objects. How do I do this? Very first time with swift. thanks
here are example files
init(title: String, locationName: String, discipline: String, coordinate: CLLocationCoordinate2D) {
self.title = title
self.locationName = locationName
self.discipline = discipline
self.coordinate = coordinate
super.init()
}
class func fromJSON(json: [JSONValue]) -> Artwork? {
// 1
var title: String
if let titleOrNil = json[16].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json[12].string
let discipline = json[15].string
// 2
let latitude = (json[18].string! as NSString).doubleValue
let longitude = (json[19].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}
var subtitle: String {
return locationName
}
// MARK: - MapKit related methods
// pinColor for disciplines: Sculpture, Plaque, Mural, Monument, other
func pinColor() -> MKPinAnnotationColor {
switch discipline {
case "Sculpture", "Plaque":
return .Red
case "Mural", "Monument":
return .Purple
default:
return .Green
}
}
// annotation callout opens this mapItem in Maps app
func mapItem() -> MKMapItem {
let addressDict = [String(kABPersonAddressStreetKey): self.subtitle]
let placemark = MKPlacemark(coordinate: self.coordinate, addressDictionary: addressDict)
let mapItem = MKMapItem(placemark: placemark)
mapItem.name = self.title
return mapItem
}
Snippet of JSON file
[
198,
"9E5E1F28-22AF-459A-841E-5E89B022505E",
198,
1340413921,
"436621",
1340413921,
"436621",
"{\n}",
null,
null,
"1933",
"Bronze plaque mounted on a stone with an inscription marking the site of an artesian well. Located along Wilder Avenue near Artesian Way.",
"1922 Wilder Avenue",
"http://hiculturearts.pastperfect-online.com/34250images/004/193301-2.JPG",
"1933.01",
"Plaque",
"Site of Honolulu's Pioneer Artesian Well",
"Full",
"21.30006",
"-157.827969",
[
null,
"21.30006",
"-157.827969",
null,
false
],
null
],
snippet of JSON file i want to use
{
"id": "301B2",
"name": "Wrenhurst",
"lat": "35.815864",
"lng": "-78.918893",
"status": "Act 2Q12",
"minp": "632000",
"maxp": "678000",
"annStarts": "14",
"annClosings": "0",
"bldList": "Builder example",
"vdl": "0",
"futures": "0",
"lotSize": "95'",

It looks like your example is using SwiftyJSON
You can look at the docs to get more information on how to get data out of a JSONValue object, but specifically, if I understand your question, you need to know how to get values using keys rather than position.
OK, it depends on what values you are getting and whether or not those values can be null in your JSON file (aka nil in Swift)
I'm going to quote the example that you gave and then show you how to get that value from your JSON file
If you know for a fact that a value will never be null in your JSON file and will always be provided:
// The code from your example using array position
let locationName = json[12].string
// Would convert to:
let locationName = json["name"].string
// And in the case of lat / lng
let latitude = (json[18].string! as NSString).doubleValue
// This converts to:
let latitude = (json["lat"].string! as NSString).doubleValue
Simply use the key instead of the position to get at your value. However, this is not the only change you need. Instead of passing an array of JSONValue objects to your factory method, you need to specify a normal JSONValue (or just a JSON object)
So your factory method changes from this:
class func fromJSON(json: [JSONValue]) -> Artwork? {
// 1
var title: String
if let titleOrNil = json[16].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json[12].string
let discipline = json[15].string
// 2
let latitude = (json[18].string! as NSString).doubleValue
let longitude = (json[19].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}
To something like this:
// This class method should return an Artwork object or nil if one couldn't be created for any reason.
// In your example, I couldn't find any reason you would return nil, so... in this example, it simply returns and Artwork object
class func fromJSON(json: JSON) -> Artwork {
// 1
var title: String
if let titleOrNil = json["title"].string {
title = titleOrNil
} else {
title = ""
}
let locationName = json["name"].string
let discipline = json["discipline"].string
// 2
let latitude = (json["lat"].string! as NSString).doubleValue
let longitude = (json["lng"].string! as NSString).doubleValue
let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
// 3
return Artwork(title: title, locationName: locationName!, discipline: discipline!, coordinate: coordinate)
}

Related

Getting latitude and longitude from a json file to create a map

I wanted to modify apple's earthquakes project to display a map together with the location magnitude and time. In the json file I can see the coordinates but for the life of me I cannot read them and use them as latitude and longitude for the map. I succeeded to display the map by using the address (title) but the format changes and there are too many possibilities to account for.
The earthquake project can be downloaded at https://developer.apple.com/documentation/coredata/loading_and_displaying_a_large_data_feed
I post the Quake.swift file below so you may have an idea of what I tried. I added a coordinates characteristic to their magnitude, place and time first as an array and then as a string but I always fail to read it and use it to display the map as latitude and longitude.
Thanks in advance for your help.
The json file is long so I post a few lines here to give you an idea of the format:
{"type":"FeatureCollection","metadata":{"generated":1648109722000,"url":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.geojson","title":"USGS All Earthquakes, Past Month","status":200,"api":"1.10.3","count":9406},"features":[{"type":"Feature","properties":{"mag":4.5,"place":"south of the Fiji Islands","time":1648106910967,"updated":1648108178040,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/us7000gwsr","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/us7000gwsr.geojson","felt":null,"cdi":null,"mmi":null,"alert":null,"status":"reviewed","tsunami":0,"sig":312,"net":"us","code":"7000gwsr","ids":",us7000gwsr,","sources":",us,","types":",origin,phase-data,","nst":null,"dmin":5.374,"rms":1.03,"gap":102,"magType":"mb","type":"earthquake","title":"M 4.5 - south of the Fiji Islands"},"geometry":{"type":"Point","coordinates":[179.1712,-24.5374,534.35]},"id":"us7000gwsr"},
{"type":"Feature","properties":{"mag":1.95000005,"place":"2 km NE of Pāhala, Hawaii","time":1648106708550,"updated":1648106923140,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/hv72960677","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/hv72960677.geojson","felt":null,"cdi":null,"mmi":null,"alert":null,"status":"automatic","tsunami":0,"sig":59,"net":"hv","code":"72960677","ids":",hv72960677,","sources":",hv,","types":",origin,phase-data,","nst":33,"dmin":null,"rms":0.109999999,"gap":136,"magType":"md","type":"earthquake","title":"M 2.0 - 2 km NE of Pāhala, Hawaii"},"geometry":{"type":"Point","coordinates":[-155.463333129883,19.2151660919189,34.9500007629395]},"id":"hv72960677"},
{"type":"Feature","properties":{"mag":1.75,"place":"4km SE of Calabasas, CA","time":1648106545420,"updated":1648109717670,"tz":null,"url":"https://earthquake.usgs.gov/earthquakes/eventpage/ci39976447","detail":"https://earthquake.usgs.gov/earthquakes/feed/v1.0/detail/ci39976447.geojson","felt":7,"cdi":3.1,"mmi":null,"alert":null,"status":"automatic","tsunami":0,"sig":49,"net":"ci","code":"39976447","ids":",ci39976447,","sources":",ci,","types":",dyfi,nearby-cities,origin,phase-data,scitech-link,","nst":33,"dmin":0.04554,"rms":0.27,"gap":56,"magType":"ml","type":"earthquake","title":"M 1.8 - 4km SE of Calabasas, CA"},"geometry":{"type":"Point","coordinates":[-118.61,34.1285,2.92]},"id":"ci39976447"},
The Quake.swift file:
import CoreData
import SwiftUI
import OSLog
// MARK: - Core Data
/// Managed object subclass for the Quake entity.
class Quake: NSManagedObject, Identifiable {
// The characteristics of a quake.
#NSManaged var magnitude: Float
#NSManaged var place: String
#NSManaged var time: Date
#NSManaged var coordinates: String
// A unique identifier used to avoid duplicates in the persistent store.
// Constrain the Quake entity on this attribute in the data model editor.
#NSManaged var code: String
/// Updates a Quake instance with the values from a QuakeProperties.
func update(from quakeProperties: QuakeProperties) throws {
let dictionary = quakeProperties.dictionaryValue
guard let newCode = dictionary["code"] as? String,
let newMagnitude = dictionary["magnitude"] as? Float,
let newPlace = dictionary["place"] as? String,
let newTime = dictionary["time"] as? Date,
let newCoordinates = dictionary["coordinates"] as? String
else {
throw QuakeError.missingData
}
code = newCode
magnitude = newMagnitude
place = newPlace
time = newTime
coordinates = newCoordinates
}
}
// MARK: - SwiftUI
extension Quake {
/// The color which corresponds with the quake's magnitude.
var color: Color {
switch magnitude {
case 0..<1:
return .green
case 1..<2:
return .yellow
case 2..<3:
return .orange
case 3..<5:
return .red
case 5..<Float.greatestFiniteMagnitude:
return .init(red: 0.8, green: 0.2, blue: 0.7)
default:
return .gray
}
}
/// An earthquake for use with canvas previews.
static var preview: Quake {
let quakes = Quake.makePreviews(count: 1)
return quakes[0]
}
#discardableResult
static func makePreviews(count: Int) -> [Quake] {
var quakes = [Quake]()
let viewContext = QuakesProvider.preview.container.viewContext
for index in 0..<count {
let quake = Quake(context: viewContext)
quake.code = UUID().uuidString
quake.time = Date().addingTimeInterval(Double(index) * -300)
quake.magnitude = .random(in: -1.1...10.0)
quake.place = "15km SSW of Cupertino, CA"
quake.coordinates = "-117.7153333,35.8655,7.59"
quakes.append(quake)
}
return quakes
}
}
// MARK: - Codable
/// creating or updating Quake instances.
struct GeoJSON: Decodable {
private enum RootCodingKeys: String, CodingKey {
case features
}
private enum FeatureCodingKeys: String, CodingKey {
case properties
}
private(set) var quakePropertiesList = [QuakeProperties]()
init(from decoder: Decoder) throws {
let rootContainer = try decoder.container(keyedBy: RootCodingKeys.self)
var featuresContainer = try rootContainer.nestedUnkeyedContainer(forKey: .features)
while !featuresContainer.isAtEnd {
let propertiesContainer = try featuresContainer.nestedContainer(keyedBy: FeatureCodingKeys.self)
// Decodes a single quake from the data, and appends it to the array, ignoring invalid data.
if let properties = try? propertiesContainer.decode(QuakeProperties.self, forKey: .properties) {
quakePropertiesList.append(properties)
}
}
}
}
/// A struct encapsulating the properties of a Quake.
struct QuakeProperties: Decodable {
// MARK: Codable
private enum CodingKeys: String, CodingKey {
case magnitude = "mag"
case place
case time
case code
case coordinates
}
let magnitude: Float // 1.9
let place: String // "21km ENE of Honaunau-Napoopoo, Hawaii"
let time: Double // 1539187727610
let code: String // "70643082"
let coordinates: String // [-117.7153333,35.8655,7.59]
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let rawMagnitude = try? values.decode(Float.self, forKey: .magnitude)
let rawPlace = try? values.decode(String.self, forKey: .place)
let rawTime = try? values.decode(Double.self, forKey: .time)
let rawCode = try? values.decode(String.self, forKey: .code)
let rawCoordinates = try? values.decode(String.self, forKey: .coordinates)
// Ignore earthquakes with missing data.
guard let magntiude = rawMagnitude,
let place = rawPlace,
let time = rawTime,
let code = rawCode,
let coordinates = rawCoordinates
else {
let values = "code = \(rawCode?.description ?? "nil"), "
+ "mag = \(rawMagnitude?.description ?? "nil"), "
+ "place = \(rawPlace?.description ?? "nil"), "
+ "time = \(rawTime?.description ?? "nil"), "
+ "coordinates = \(rawCoordinates?.description ?? "nil")"
let logger = Logger(subsystem: "com.example.apple-samplecode.Earthquakes", category: "parsing")
logger.debug("Ignored: \(values)")
throw QuakeError.missingData
}
self.magnitude = magntiude
self.place = place
self.time = time
self.code = code
self.coordinates = coordinates
}
// The keys must have the same name as the attributes of the Quake entity.
var dictionaryValue: [String: Any] {
[
"magnitude": magnitude,
"place": place,
"time": Date(timeIntervalSince1970: TimeInterval(time) / 1000),
"code": code,
"coordinates": coordinates
]
}
}
The coordinates are not on the same level as properties, they are in a sibling geometry. The basic pattern is
{
"features": [
{
"properties": {
"mag":1.9,
"place":"21km ENE of Honaunau-Napoopoo, Hawaii",
"time":1539187727610,"updated":1539187924350,
"code":"70643082"
},
"geometry" : {
"coordinates": [-122.8096695,38.8364983,1.96]
}
}
]
}
You have to decode the coordinates in GeoJSON by adding geometry to FeatureCodingKeys. And you have to extend the Core Data model to preserve the coordinates.
In the Core Data model add two properties
longitude - Double - non-optional, use scalar type
latitude - Double - non-optional, use scalar type
In Quake.swift
import CoreLocation
In the class Quake add
#NSManaged var latitude: CLLocationDegrees
#NSManaged var longitude: CLLocationDegrees
and replace update(from with
func update(from quakeProperties: QuakeProperties) throws {
let dictionary = quakeProperties.dictionaryValue
guard let newCode = dictionary["code"] as? String,
let newMagnitude = dictionary["magnitude"] as? Float,
let newPlace = dictionary["place"] as? String,
let newTime = dictionary["time"] as? Date,
let newLatitude = dictionary["latitude"] as? CLLocationDegrees,
let newLongitude = dictionary["longitude"] as? CLLocationDegrees
else {
throw QuakeError.missingData
}
code = newCode
magnitude = newMagnitude
place = newPlace
time = newTime
latitude = newLatitude
longitude = newLongitude
}
In the Quake extension add
var coordinate : CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
In GeoJSON extend FeatureCodingKeys
private enum FeatureCodingKeys: String, CodingKey {
case properties, geometry
}
and replace the while loop with
while !featuresContainer.isAtEnd {
let propertiesContainer = try featuresContainer.nestedContainer(keyedBy: FeatureCodingKeys.self)
// Decodes a single quake from the data, and appends it to the array, ignoring invalid data.
if var properties = try? propertiesContainer.decode(QuakeProperties.self, forKey: .properties),
let geometry = try? propertiesContainer.decode(QuakeGeometry.self, forKey: .geometry) {
let coordinates = geometry.coordinates
properties.longitude = coordinates[0]
properties.latitude = coordinates[1]
quakePropertiesList.append(properties)
}
}
Add the struct
struct QuakeGeometry: Decodable {
let coordinates : [Double]
}
In QuakeProperties add
var latitude : CLLocationDegrees = 0.0
var longitude : CLLocationDegrees = 0.0
and replace dictionaryValue with
var dictionaryValue: [String: Any] {
[
"magnitude": magnitude,
"place": place,
"time": Date(timeIntervalSince1970: TimeInterval(time) / 1000),
"code": code,
"latitude": latitude,
"longitude": longitude
]
}
Finally in DetailView.swift
import MapKit
and replace QuakeDetail with
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.3, longitudeDelta: 0.3)))
}
var body: some View {
VStack {
QuakeMagnitude(quake: quake)
Text(quake.place)
.font(.title3)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) - \(quake.longitude)")
Map(coordinateRegion: $region, annotationItems: [quake]) { item in
MapMarker(coordinate: item.coordinate, tint: .red)
}
}
}
}

How to Decode Badly Formatted JSON in Swift using Codable

I am trying to decode the Yelp REST API business search response in Swift using codable. I have managed to decode this using the traditional method of serializing the response into a NSDictionary and just marshaling objects using values from the dictionary; however, as an exercise to myself I am trying to refactor the code to use the Codable protocols and JSONDecoder() in Swift. Yelp really wrote a shitty API and this is the response that I need to decode:
{
businesses = (
{
alias = "dos-caminos-new-york-7";
categories = (
{
alias = mexican;
title = Mexican;
},
{
alias = bars;
title = Bars;
}
);
coordinates = {
latitude = "40.7593727";
longitude = "-73.9853281";
};
"display_phone" = "(212) 918-1330";
distance = "55.57057440108928";
id = 2iwT3iutZvmqzmu7oOkWFw;
"image_url" = "https://s3-media2.fl.yelpcdn.com/bphoto/eHvXPqv6iLcTXfT486z6JA/o.jpg";
"is_closed" = 0;
location = {
address1 = "1567 Broadway";
address2 = "";
address3 = "";
city = "New York";
country = US;
"display_address" = (
"1567 Broadway",
"New York, NY 10036"
);
state = NY;
"zip_code" = 10036;
};
name = "Dos Caminos";
phone = "+12129181330";
price = "$$";
rating = "3.5";
"review_count" = 644;
transactions = (
delivery,
pickup
);
url = "https://www.yelp.com/biz/dos-caminos-new-york-7?adjust_creative=RzPd81IBxDPwaWBeNhRk8w&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=RzPd81IBxDPwaWBeNhRk8w";
},
{
alias = "bond-45-new-york-2";
categories = (
{
alias = italian;
title = Italian;
},
{
alias = pizza;
title = Pizza;
},
{
alias = newamerican;
title = "American (New)";
}
);
coordinates = {
latitude = "40.7591637016814";
longitude = "-73.9859788257256";
};
"display_phone" = "(212) 869-4545";
distance = "77.44246334243893";
id = "8-Ug0Zjs5sL6MARspRJ7CA";
"image_url" = "https://s3-media2.fl.yelpcdn.com/bphoto/CEfwtx0YtfDGog0hWmQ72A/o.jpg";
"is_closed" = 0;
location = {
address1 = "221 W 46th St";
address2 = "";
address3 = "";
city = "New York";
country = US;
"display_address" = (
"221 W 46th St",
"New York, NY 10036"
);
state = NY;
"zip_code" = 10036;
};
name = "Bond 45";
phone = "+12128694545";
price = "$$";
rating = "3.5";
"review_count" = 755;
transactions = (
delivery,
pickup
);
url = "https://www.yelp.com/biz/bond-45-new-york-2?adjust_creative=RzPd81IBxDPwaWBeNhRk8w&utm_campaign=yelp_api_v3&utm_medium=api_v3_business_search&utm_source=RzPd81IBxDPwaWBeNhRk8w";
}
);
region = {
center = {
latitude = "40.758896";
longitude = "-73.98513";
};
};
total = 5300;
}
The top level object has a value of businesses which has a list? of Businesses that I want to create. I have created the corresponding Swift Codable structs as such:
struct Response: Codable {
var businesses: [Business]
enum CodingKeys: CodingKey {
case businesses
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
// ERROR: Swift.DecodingError.typeMismatch(Swift.Dictionary<Swift.String, Any>, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "businesses", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0)], debugDescription: "Expected to decode Dictionary<String, Any> but found an array instead.", underlyingError: nil))).
businesses = try values.decode([Business].self, forKey: .businesses)
print("values is \(values.allKeys)")
}
func encode(to encoder: Encoder) throws {
//TODO
}
}
and
struct Business: Codable {
var name: String
var category:
var latitude: Double = 0.0
var longitude: Double = 0.0
var phoneNumber: String = ""
var distance: Double = 0.0
var imageURL: String = ""
var price: Price
var yelpURL: String
enum CodingKeys: String, CodingKey {
case name = "name"
case category = "categories"
case coordinates = "coordinates"
case phoneNumber = "phone"
case distance = "distance"
case imageURL = "image_url"
case price = "price"
case yelpURL = "url"
}
enum CategoryCodingKey: String, CodingKey {
case alias = "alias"
case title = "title"
}
enum CoordinateInfoKeys: String, CodingKey {
case latitude = "latitude"
case longitude = "longitude"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
// TODO, will add more here but the error is not really here
}
func encode(to encoder: Encoder) throws {
}
}
The error that I get when I try to run this code is in a comment in the Response struct above. It seems to me that the YELP API is formatted in such a way that the value for businesses is not a list of objects but some other type that Swift does not recognize. How do I go about using Codable to decode this Response into the Response struct that will hold a list of Businesses? Is this even possible with the way the YELP API has been setup?

I'm not able to convert the array of objects to format that can be send and store to web server

I'm trying to send array of objects to the web server using urlsession. I need to convert the array of objects into some format than can be send and store into the server.
model.swift
class UserDetails
{
var name: String
var phNumber: Int
var pincode : Int
var numberOfItems: Int
init(name: String, number: Int, pin : Int, items: Int) {
self.name = name
self.phNumber = number
self.pincode = pin
self.numberOfItems = items
}
}
/* viewController.swift */
var users: [UserDetails] = []
func sendLabDetails(userId: Int,users: [UserDetails], completion: #escaping (_ serverResponse: NSDictionary) -> Void)
{
let parameters = ["user_id": userId, "users": users] as [String : Any]
}
I think I'm doing wrong in calling the API with objects. Please help me with some code on how to convert the array of objects into array of dictionaries or any other JSON supported format.
If you have a Dictionary you can convert it to JSON using JSONEncoder:
let parameters = ["user_id": "newID", "users": "newUser"]
do{
let jsonParameter = try JSONEncoder().encode(parameters)
}catch{
print(error.localizedDescription)
}
NOTE: To encode an object means that this object conforms to protocol Codable. Any does not conform to this protocol, so it cannot be encoded as JSON.
In order to do this, you can make your class UserDetails to conform that protocol or you have to change your data structure, because [String:Any] is not good.
EXAMPLE:
class UserDetails: Codable //Be sure to make the class to conform to this protocol if needed
{
var name: String
var phNumber: Int
var pincode : Int
var numberOfItems: Int
init(name: String, number: Int, pin : Int, items: Int) {
self.name = name
self.phNumber = number
self.pincode = pin
self.numberOfItems = items
}
}
//Now you can encode:
var users: [UserDetails] = []
func sendLabDetails(userId: Int,users: [UserDetails], completion: #escaping (_ serverResponse: NSDictionary) -> Void)
{
let parameters = ["user_id": userId, "users": users]
do{
let jsonParameter = try JSONEncoder().encode(parameters)
//HERE YOU CAN USE jsonParameter to send data to server
}catch{
print(error.localizedDescription)
}
}

How to insert JSON array model in Swift?

I have JSON like this but with more data:
[
{
"name": "Place 1",
"avatar": "https://sometext.it/image=1",
"id": "1",
"lng": 10.01,
"lat": 15.02
},
{
"name": "Place 2",
"avatar": "https://sometext.it/image=2",
"id": "2",
"lng": 15.02,
"lat": 15.03
}
]
I get JSON from URL and I want to insert them to array of places. I have class:
class Place {
var Avatar = ""
var Id = 0
var Lat = 0.0
var Lng = 0.0
var Name = ""
required init(avatar: String, id: Int, lat: Double, lng: Double, name: String) {
self.Avatar = avatar
self.Id = id
self.Lat = lat
self.Lng = lng
self.Name = name
}
}
And i create an Array:
var places: [Place] = []
I serialize JSON like this:
func parsingJson() {
guard let url = URL(string: "https://somelink.com") else {
return
}
let session = URLSession.shared
session.dataTask(with: url) { (data, response, error) in
if let response = response {
print(response)
}
if let data = data {
print(data)
do {
let json = try JSONSerialization.jsonObject(with: data, options: [])
for result in json as! [[String:Any]] {
let avatar = result["avatar"] as! String
let id = result["id"] as! Int
let lat = result["lat"] as! Double
let lng = result["lng"] as! Double
let name = result["name"] as! String
let place = Place(avatar: avatar, id: id, lat: lat, lng: lng, name: name)
self.places.append(place)
print(result)
}
}catch {
print("JSON Error")
}
}
}.resume()
}
but that doesn't work, I have error like this: http://obrazki.elektroda.pl/9267167800_1497627255.png
I know that I have nil but I don't know why :( When I print the JSON when I'm serialising I see it on console.
The first, you are trying to get ID as an INT, when your JSON Object is showing that it's a string.. So you would need to do:
var id = Int()
if let someID = result["id"] as? String {
id = Int(someId)
} else {
print("ID failed as String")
}
However, i'd also recommend using a guard statement before your for loop:
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [[String:Any]] else {
print("Invalid json object type")
return
}
for result in json {
///your code.
}
you at least try to unwrap the variable, and get some Int instance to pass along
But obviously, the simpler solution would be to save the id field in your JSON to the type you would want on your front-end.. in this case a Number.
While it might not look as clean as your code, use unwraping methods like guards, and if-else logics as much as possible, and create failed unwrap fallbacks as much as possible as early on in your project. It helps in debugging down the line and creates a good base when your project becomes large enough.
Check up on Apple's guide and this here for some good starting points.
Late update, but re-reading on all this landed me on this neat article by Apple:
https://developer.apple.com/swift/blog/?id=37
You haven't nil, read error - Could not cast value of type NSTaggedPointerString to NSNumber This means that your id is String and you are casting it to Int so replace
let id = result["id"] as! Int
to
let id = result["id"] as! String

Best way to convert JSON or other untyped data into typed classes in Swift?

I'm trying to parse out JSON into typed classes for safety/convenience, but it's proving very clunky. I wasn't able to find a library or even a post for Swift (Jastor is as close as I got). Here's a fabricated little snippet to illustrate:
// From NSJSONSerialization or similar and casted to an appropriate toplevel type (e.g. Dictionary).
var parsedJson: Dictionary<String, AnyObject> = [ "int" : 1, "nested" : [ "bool" : true ] ]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(fromParsedJson json: NSDictionary) {
if let parsedStringValue = json["string"] as? String {
self.stringValueWithDefault = parsedStringValue
}
if let parsedIntValue = json["int"] as? Int {
self.intValueRequired = parsedIntValue
} else {
// Raise an exception...?
}
// Optional-chaining is actually pretty nice for this; it keeps the blocks from nesting absurdly.
if let parsedBool = json["nested"]?["bool"] as? Bool {
self.nestedBoolBroughtToTopLevel = parsedBool
}
if let parsedFirstInt = json["firstInt"] as? Int {
if let parsedSecondInt = json["secondInt"] as? Int {
self.combinedIntRequired = parsedFirstInt * parsedSecondInt
}
}
// Most succinct way to error if we weren't able to construct self.combinedIntRequired?
}
}
TypedObject(fromParsedJson: parsedJson)
There's a number of issues here that I'm hoping to work around:
It's extremely verbose, since I need to wrap every single property in a copy-pasted if-let for safety.
I'm not sure how to communicate errors when required properties are missing (as noted above). Swift seems to prefer (?) using exceptions for show-stopping problems (rather than pedestrian malformed data as here).
I don't know a nice way to deal with properties that exist but are the wrong type (given that the as? casting will fail and simply skip the block, it's not very informative to the user).
If I want to translate a few properties into a single one, I need to nest the let blocks proportional to the number of properties I'm combining. (This is probably more generally a problem with combining multiple optionals into one value safely).
In general, I'm writing imperative parsing logic when I feel like I ought to be able to do something a little more declarative (either with some stated JSON schema or at least inferring the schema from the class definition).
I do this using the Jastor framework:
1) Implement a Protocol that has a single function that returns an NSDictionary response:
protocol APIProtocol {
func didReceiveResponse(results: NSDictionary)
}
2) Create an API class that defines an NSURLConnection object that can be used as a Request URL for iOS's networking API. This class is created to simply return a payload from the itunes.apple.com API.
class API: NSObject {
var data: NSMutableData = NSMutableData()
var delegate: APIProtocol?
func searchItunesFor(searchTerm: String) {
// Clean up the search terms by replacing spaces with +
var itunesSearchTerm = searchTerm.stringByReplacingOccurrencesOfString(" ", withString: "+",
options: NSStringCompareOptions.CaseInsensitiveSearch, range: nil)
var escapedSearchTerm = itunesSearchTerm.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
var urlPath = "https://itunes.apple.com/search?term=\(escapedSearchTerm)&media=music"
var url: NSURL = NSURL(string: urlPath)
var request: NSURLRequest = NSURLRequest(URL: url)
var connection: NSURLConnection = NSURLConnection(request: request, delegate: self, startImmediately: false)
println("Search iTunes API at URL \(url)")
connection.start()
}
// NSURLConnection Connection failed.
func connection(connection: NSURLConnection!, didFailWithError error: NSError!) {
println("Failed with error:\(error.localizedDescription)")
}
// New request so we need to clear the data object.
func connection(didReceiveResponse: NSURLConnection!, didReceiveResponse response: NSURLResponse!) {
self.data = NSMutableData()
}
// Append incoming data.
func connection(connection: NSURLConnection!, didReceiveData data: NSData!) {
self.data.appendData(data)
}
// NSURLConnection delegate function.
func connectionDidFinishLoading(connection: NSURLConnection!) {
// Finished receiving data and convert it to a JSON object.
var jsonResult: NSDictionary = NSJSONSerialization.JSONObjectWithData(data,
options: NSJSONReadingOptions.MutableContainers, error: nil) as NSDictionary
delegate?.didReceiveResponse(jsonResult)
}
}
3) Create a class with associated properties that inherits from Jastor
NSDictionary response:
{
"resultCount" : 50,
"results" : [
{
"collectionExplicitness" : "notExplicit",
"discCount" : 1,
"artworkUrl60" : "http:\/\/a4.mzstatic.com\/us\/r30\/Features\/2a\/b7\/da\/dj.kkirmfzh.60x60-50.jpg",
"collectionCensoredName" : "Changes in Latitudes, Changes in Attitudes (Ultmate Master Disk Gold CD Reissue)"
}
]
}
Music.swift
class Music : Jastor {
var resultCount: NSNumber = 0
}
4) Then in your ViewController be sure to set the delegate to self and then make a call to the API's searchITunesFor() method.
var api: API = API()
override func viewDidLoad() {
api.delegate = self;
api.searchItunesFor("Led Zeppelin")
}
5) Implement the Delegate method for didReceiveResponse(). Jastor extends your class to set a NSDictionary of the results returned from the iTunes API.
// #pragma - API Delegates
func didReceiveResponse(results: NSDictionary) {
let music = Music(dictionary: results)
println(music)
}
Short version: Since init isn't allowed to fail, validation has to happen outside of it. Optionals seem to be the intended tool for flow control in these cases. My solution is to use a factory method that returns an optional of the class, and use option chaining inside it to extract and validate the fields.
Note also that Int and Bool aren't children of AnyObject; data coming from an NSDictionary will have them stored as NSNumbers, which can't be cast directly to Swift types. Thus the calls to .integerValue and .boolValue.
Long version:
// Start with NSDictionary since that's what NSJSONSerialization will give us
var invalidJson: NSDictionary = [ "int" : 1, "nested" : [ "bool" : true ] ]
var validJson: NSDictionary = [
"int" : 1,
"nested" : [ "bool" : true ],
"firstInt" : 3,
"secondInt" : 5
]
class TypedObject {
let stringValueWithDefault: String = ""
let intValueRequired: Int
let nestedBoolBroughtToTopLevel: Bool = false
let combinedIntRequired: Int
init(intValue: Int, combinedInt: Int, stringValue: String?, nestedBool: Bool?) {
self.intValueRequired = intValue
self.combinedIntRequired = combinedInt
// Use Optionals for the non-required parameters so
// we know whether to leave the default values in place
if let s = stringValue {
self.stringValueWithDefault = s
}
if let n = nestedBool {
self.nestedBoolBroughtToTopLevel = n
}
}
class func createFromDictionary(json: Dictionary<String, AnyObject>) -> TypedObject? {
// Validate required fields
var intValue: Int
if let x = (json["int"]? as? NSNumber)?.integerValue {
intValue = x
} else {
return nil
}
var combinedInt: Int
let firstInt = (json["firstInt"]? as? NSNumber)?.integerValue
let secondInt = (json["secondInt"]? as? NSNumber)?.integerValue
switch (firstInt, secondInt) {
case (.Some(let first), .Some(let second)):
combinedInt = first * second
default:
return nil
}
// Extract optional fields
// For some reason the compiler didn't like casting from AnyObject to String directly
let stringValue = json["string"]? as? NSString as? String
let nestedBool = (json["nested"]?["bool"]? as? NSNumber)?.boolValue
return TypedObject(intValue: intValue, combinedInt: combinedInt, stringValue: stringValue, nestedBool: nestedBool)
}
class func createFromDictionary(json: NSDictionary) -> TypedObject? {
// Manually doing this cast since it works, and the only thing Apple's docs
// currently say about bridging Cocoa and Dictionaries is "Information forthcoming"
return TypedObject.createFromDictionary(json as Dictionary<String, AnyObject>)
}
}
TypedObject.createFromDictionary(invalidJson) // nil
TypedObject.createFromDictionary(validJson) // it works!
I've also done the following to convert to/from:
class Image {
var _id = String()
var title = String()
var subTitle = String()
var imageId = String()
func toDictionary(dict dictionary: NSDictionary) {
self._id = dictionary["_id"] as String
self.title = dictionary["title"] as String
self.subTitle = dictionary["subTitle"] as String
self.imageId = dictionary["imageId"] as String
}
func safeSet(d: NSMutableDictionary, k: String, v: String) {
if (v != nil) {
d[k] = v
}
}
func toDictionary() -> NSDictionary {
let jsonable = NSMutableDictionary()
self.safeSet(jsonable, k: "title", v: self.title);
self.safeSet(jsonable, k: "subTitle", v: self.subTitle);
self.safeSet(jsonable, k: "imageId", v: self.imageId);
return jsonable
}
}
Then I simply do the following:
// data (from service)
let responseArray = NSJSONSerialization.JSONObjectWithData(data, options: .MutableContainers, error: nil) as NSArray
self.objects = NSMutableArray()
for item: AnyObject in responseArray {
var image = Image()
image.toDictionary(dict: item as NSDictionary)
self.objects.addObject(image)
}
If you want to POST the data:
var image = Image()
image.title = "title"
image.subTitle = "subTitle"
image.imageId = "imageId"
let data = NSJSONSerialization.dataWithJSONObject(image.toDictionary(), options: .PrettyPrinted, error: nil) as NSData
// data (to service)
request.HTTPBody = data;