Creating a CLLocationCoordinate2D using Codable protocol - swift

I'm trying to conform a custom class to the MKAnnotation protocol. To create this class, I'm decoding from a JSON feed using the Codable protocol.
class CustomClass: NSObject, Codable, MKAnnotation {
var id: String
var name: String
var lat: Double?
var lon: Double?
var coordinate: CLLocationCoordinate2D
// Note, the coordinate var is not a part of the decoded JSON file.
// It is derived from the lat and lon attributes, which are in the
// JSON file.
enum CodingKeys: String, CodingKey {
case id
case name
case lat
case lon
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.id = try values.decode(String.self, forKey: .id)
self.name = try values.decodeIfPresent(String.self, forKey: .name)
self.lat = try values.decodeIfPresent(Double.self, forKey: .lat)
self.lon = try values.decodeIfPresent(Double.self, forKey: .lon)
self.coordinate = CLLocationCoordinate2D(latitude: self.lat!, longitude: self.lon!)
}
}
When I run this code, I get the following error in the line where I set the self.coordinate var:
Thread 2: Fatal error: Unexpectedly found nil while unwrapping an Optional value
It looks like the coordinate variable is getting set before the lat and lon variables are decoded. How can I set the coordinate var using the decoded lat and lon vars inside my init method?

Related

Swift Decodable: Inject value in nested generic property

I have this API response structure (from Strapi v4):
{
"data": [
{
"id": 1,
"attributes": {
"description": "test",
}
}
]
}
I have this generic code to handle API responses and to inject the ID to my child object:
struct StrapiArrayResponse<Content: StrapiDataObjectContent>: Codable {
var data: [StrapiDataObject<Content>]
}
struct StrapiDataObject<Content: StrapiDataObjectContent>: Codable {
let id: Int
var attributes: Content
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.attributes = try container.decode(Content.self, forKey: .attributes)
self.attributes.id = id
}
}
protocol StrapiDataObjectContent: Codable {
var id: Int! { get set } // I don't want this to be an Optional
}
I want my id to be a let instead of an optional var.
Is there a better way to inject the ID to my child objects (StrapiDataObjectContent)?
Here is a solution for the problem but it isn't so straightforward and requires some work.
Since you want id to be a constant we need a way to initialise Content with it so one way then is to add an init to the protocol.
protocol StrapiDataObjectContent: Codable {
var id: Int { get } //also removed 'set'
init(id: Int, copy: Self)
}
As you see this init takes an already existing object as parameter so this is kind of a copy method
So an implementation (based on the json in the question) could then be
init(id: Int, copy: Test) {
self.id = id
self.description = copy.description
}
We then need to change init(from:) in StrapiDataObject to
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
let attributes = try container.decode(Content.self, forKey: .attributes)
self.attributes = Content(id: id, copy: attributes)
}
Now this compiles but we will get a runtime error since id is expected by the decoder for Content but doesn't exists in the json.
So this leads to the major drawback of this solution, every type conforming to StrapiDataObjectContent needs to implement a custom init(from:) just to avoid decoding the id property
To demonstrate here is a full example (based on the json in the question)
struct Test: StrapiDataObjectContent {
let id: Int
let description: String
init(from decoder: Decoder) throws {
id = 0
let container = try decoder.container(keyedBy: CodingKeys.self)
description = try container.decode(String.self, forKey: .description)
}
init(id: Int, copy: Test) {
self.id = id
self.description = copy.description
}
}

Using Decodable with injected properties

Is there any way to use Decodable with injected property?
final class Score: Decodable {
let value: Int?
let uniqueId: String
convenience init(from decoder: Decoder/*, uniqueId: String*/) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try container.decodeIfPresent(Int.self, forKey: .value).flatMap { value = $0 }
// self.uniqueId = uniqueId
[... other properties parsing ...]
}
}
Example call:
final class Exam {
let identifier: Int
let scores: [Score]
convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
identifier = try container.decode(Int.self, forKey: .identifier)
scores = try container.decode([Score].self, forKey: .scores)
// I need to pass Exam's identifier to `score` on init, because it will generate Score's `uniqueId `
[... other properties parsing ...]
}
}
That would end with error with missing uniqueId, which I need to have after init but it's not in the JSON. Since it's identifier, making it optional and setting outside is not a proper way to handle it.
I'd love to inject it the way it's commented above, but how to do it?
There is no way to extend the initialiser because it's being called indirectly and there is no API provided to extend it. Thus, there are several ways to bypass it:
BEST: Inject the value into Decoder's userInfo if possible.
Create separate class for Response and separate for model. Example below.
Use plain JSONSerialization instead of Decodable.
As #JoakimDanielson suggested, create random identifier inside default initialiser. The issue is that it's not reproducable, so in case you're saving it to the DB, you'll always override the data, since the ID with each parsing would be different.
Example for approach 2:
final class ScoreResponse: Decodable {
let value: Int?
convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try container.decodeIfPresent(Int.self, forKey: .value).flatMap { value = $0 }
[... other properties parsing ...]
}
}
final class Score {
let value: Int?
let uniqueId: String
convenience init(from response: ScoreResponse, uniqueId: String) {
self.value = response.value // etc with other properties
self.uniqueId = uniqueId
}
}
final class Exam: Decodable {
let identifier: String
let scores: [Score] = []
convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
identifier = try container.decode(String.self, forKey: .identifier)
try container.decodeIfPresent([ScoreResponse].self, forKey: .scores).forEach {
scores.append({ Score(from: $0, uniqueId: identifier) })
}
}

Swift - Codable Decode array of arrays of CLLocation

I have a struct that contains an array of arrays of CLLocations. This is to support a multipolyline (in other words, a bunch of potentially discontiguous lines). I wish to encode and decode this data. I am having trouble writing the encoding and decoding methods as CLLocation is not codable by default.
struct MyTrack {
let coords: [[CLLocation]]?
enum CodingKeys: String, CodingKey {
case coords
}
}
extension MyTrack: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
coords = try values.decodeIfPresent([[CLLocation]].self, forKey: .coords)?
.map { ($0 as AnyObject).map { CLLocation(model: $0) } }
}
}
Its currently throwing two errors in Xcode:
Cannot convert value of type '[[CLLocation]].Type' to expected argument type '[Any?].Type'
Value of type 'AnyObject' has no member 'map'
Any help much appreciated!
Because CLLocation is not Codable by default, I followed a tutorial to create a wrapper struct around it, the code goes like this:
extension CLLocation: Encodable {
enum CodingKeys: String, CodingKey {
case latitude
case longitude
case altitude
case horizontalAccuracy
case verticalAccuracy
case speed
case course
case timestamp
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(coordinate.latitude, forKey: .latitude)
try container.encode(coordinate.longitude, forKey: .longitude)
try container.encode(altitude, forKey: .altitude)
try container.encode(horizontalAccuracy, forKey: .horizontalAccuracy)
try container.encode(verticalAccuracy, forKey: .verticalAccuracy)
try container.encode(speed, forKey: .speed)
try container.encode(course, forKey: .course)
try container.encode(timestamp, forKey: .timestamp)
}
}
struct Location: Codable {
let latitude: CLLocationDegrees
let longitude: CLLocationDegrees
let altitude: CLLocationDistance
let horizontalAccuracy: CLLocationAccuracy
let verticalAccuracy: CLLocationAccuracy
let speed: CLLocationSpeed
let course: CLLocationDirection
let timestamp: Date
}
extension CLLocation {
convenience init(model: Location) {
self.init(coordinate: CLLocationCoordinate2DMake(model.latitude, model.longitude), altitude: model.altitude, horizontalAccuracy: model.horizontalAccuracy, verticalAccuracy: model.verticalAccuracy, course: model.course, speed: model.speed, timestamp: model.timestamp)
}
}
You are decoding CLLocation, not your wrapper struct. You should decode your wrapper struct instead. Also, you shouldn't cast to AnyObject.
extension MyTrack: Decodable {
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
coords = try values.decodeIfPresent([[Location]].self, forKey: .coords)?
.map { $0.map(CLLocation.init) }
}
}

Access property of parent struct in a nested Codable struct when decoding the child

When using a decoder in a nested Codable struct, is there any way to access a property of a parent struct?
The only way I can think of that might work (haven't tested yet) is to use a manual decoder in the parent struct too, set the property in the userInfo dictionary, and then access userInfo in the child struct. But that would result in a lot of boilerplate code. I'm hoping there's a simpler solution.
struct Item: Decodable, Identifiable {
let id: String
let title: String
let images: Images
struct Images: Decodable {
struct Image: Decodable, Identifiable {
let id: String
let width: Int
let height: Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
width = try container.decode(Int.self, forKey: .width)
height = try container.decode(Int.self, forKey: .height)
// How do I get `parent.parent.id` (`Item#id`) here?
id = "\(parent.parent.id)\(width)\(height)"
}
}
let original: Image
let small: Image
// …
}
}
In the above example, the item ID coming from the server is only defined in the top-level properties in the JSON, but I need them in the children too, so I can also make them Identifiable.
I managed it using Itai Ferber's suggestion as mentioned by #New Dev in the following way:
Create a new reference type whose only purpose is to contain a
mutable value that can be passed between parent and child.
Assign an instance of that type to the JSONDecoder's userInfo dictionary.
Retrieve that instance when decoding the parent and assign to it the id that you're interested in passing.
Whilst decoding the child, retrieve that id from the instance stored in the userInfo earlier.
I've modified your example above as follows:
struct Item: Decodable, Identifiable {
enum CodingKeys: String, CodingKey {
case id
case title
case images
}
let id: String
let title: String
let images: Images
struct Images: Decodable {
struct Image: Decodable, Identifiable {
let id: String
let width: Int
let height: Int
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
width = try container.decode(Int.self, forKey: .width)
height = try container.decode(Int.self, forKey: .height)
if let referenceTypeUsedOnlyToContainAChangeableIdentifier = decoder.userInfo[.referenceTypeUsedOnlyToContainAChangeableIdentifier] as? ReferenceTypeUsedOnlyToContainAChangeableIdentifier {
self.id = referenceTypeUsedOnlyToContainAChangeableIdentifier.changeableIdentifier
} else {
self.id = "something went wrong"
}
}
}
let original: Image
let small: Image
// …
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
if let referenceTypeUsedOnlyToContainAChangeableIdentifier = decoder.userInfo[.referenceTypeUsedOnlyToContainAChangeableIdentifier] as? ReferenceTypeUsedOnlyToContainAChangeableIdentifier {
referenceTypeUsedOnlyToContainAChangeableIdentifier.changeableIdentifier = id
}
}
}
}
// Use this reference type to just store an id that's retrieved later.
class ReferenceTypeUsedOnlyToContainAChangeableIdentifier {
var changeableIdentifier: String?
}
// Convenience extension.
extension CodingUserInfoKey {
static let referenceTypeUsedOnlyToContainAChangeableIdentifier = CodingUserInfoKey(rawValue: "\(ReferenceTypeUsedOnlyToContainAChangeableIdentifier.self)")!
}
let decoder = JSONDecoder()
// Assign the reference type here to be used later during the decoding process first to assign the id in `Item` and then
// later to retrieve that value in `Images`
decoder.userInfo[.referenceTypeUsedOnlyToContainAChangeableIdentifier] = ReferenceTypeUsedOnlyToContainAChangeableIdentifier()

RLMArray in swift with decoder : Ambiguous reference to member error

I want to use Realm in mixed Objective-C & Swift app working with Codable and Realm Object can be export to Objective-C ;
class Person2 : RLMObject,Decodable {
#objc dynamic var name = ""
convenience init(_ name:String) {
self.init()
self.name = name
}
}
class RepairShop2 : RLMObject,Decodable {
#objc dynamic var name = ""
#objc dynamic var contact:Person2?
#objc dynamic var persons = RLMArray<Person2>(objectClassName: Person2.className())
private enum RepairShop2CodingKeys: String, CodingKey {
case name
case contact
case persons
}
convenience init(name: String, contact: Person2, persons: RLMArray<Person2>) {
self.init()
self.name = name
self.contact = contact
self.persons = persons
}
convenience required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RepairShop2CodingKeys.self)
let name = try container.decode(String.self, forKey: .name)
let contact = try container.decode(Person2.self, forKey: .contact)
let personArray = try container.decode(RLMArray<AnyObject>, forKey: .persons)
// this line error: Ambiguous reference to member 'decode(_:forKey:)'**
let persons = RLMArray<Person2>(objectClassName: Person2.className())
persons.addObjects(personArray)
self.init(name: name, contact: contact, persons: persons)
}
}
let personArray = try container.decode(RLMArray<AnyObject>, forKey: .persons)
// this line error: Ambiguous reference to member 'decode(_:forKey:)'**
RLMArray.self I also tried , fail
how to write decode type of RLMArray?
RLMRealm doesn't conform to Decodable so you'll not be able to parse it into RLMRealm straight away. Instead try something like:
let persons = RLMArray<Person2>(objectClassName: Person2.className())
persons.addObjects(try container.decode([Person2].self, forKey: .persons) as NSFastEnumeration)
As a side note. Mixing different domains into one model is bad idea, it may bite later.