16

I am hoping here to get an understanding of this error and perhaps a broader understanding of encodable and decodable. Part of my class looks as follows:

  public var eventId: String?
  public var eventName: String?
  public var eventDescription: String?
  public var location: CLLocation?

  /// These properties will be encoded/decoded from JSON
  private enum CodingKeys: String, CodingKey {
    case eventId
    case eventName
    case eventDescription
    case location
  }

  public required convenience init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)

    let eventId = try container.decode(String?.self, forKey: .eventId)
    let eventName = try container.decode(String?.self, forKey: .eventName)
    let location = try container.decode(CLLocation?.self, forKey: .location)
    self.init(eventId: eventId, eventName: eventName, location:location)
  }

This class works perfectly, until I add location. When I do I get two errors: Type 'CAEvent' does not conform to protocol 'Encodable', and 'Reference to member 'location' cannot be resolved without a contextual type' inside the fromDecoder method. Could someone explain the issue?

AlexK
  • 336
  • 8
  • 21
  • 41
  • do implement something to make it conform or is there an alternate way to store location data? – AlexK Sep 06 '18 at 05:50
  • 4
    You could store the latitude and longitude as numbers. – matt Sep 06 '18 at 05:51
  • Do you know if that is standard practice? – AlexK Sep 06 '18 at 05:56
  • What isn’t standard practice is to imagine you can stuff a CLLocation into a JSON. What exactly are you trying to do? – matt Sep 06 '18 at 06:52
  • @AlexKornhauser Thank you for asking your questions. I am also trying to bridge the gap between Firestore's GeoPoint and Swift's CLLocation. It's proving to be quite a challenge. Were you able to resolve this finally? – Zonker.in.Geneva Aug 12 '19 at 21:48

2 Answers2

26

I google and found an article, which provide implementations for un-codable CLLocation.

After reading that article, it's hard to implement Decodable for CLLocation. But the author use another struct Location for decoding CLLocation object. It's funny and tricky.


For Encodable

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)
    }
}

For Decodable

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)
     }
}


/// 
struct Person {
    let name: String
    let location: CLLocation
    enum CodingKeys: String, CodingKey {
        case name
        case location
    }
}
extension Person: Decodable {
    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)           
        let name = try values.decode(String.self, forKey: .name)

        // Decode to `Location` struct, and then convert back to `CLLocation`. 
        // It's very tricky
        let locationModel = try values.decode(Location.self, forKey: .location)
        location = CLLocation(model: locationModel)
    }
}
AechoLiu
  • 17,522
  • 9
  • 100
  • 118
0

Depending on what you want your location to contain, you could add a second JSON compatible variable which is handled in the decoder to create a CLLocation. This isn't decoding a complete CLLocation but might be all you need

public var eventId: String?
public var eventName: String?
public var eventDescription: String?
public var location: [Float]? // latitude, longitude
public var cllocation: CLLocation?

/// These properties will be encoded/decoded from JSON
private enum CodingKeys: String, CodingKey {
case eventId
case eventName
case eventDescription
case location
}

public required convenience init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let eventId = try container.decode(String?.self, forKey: .eventId)
let eventName = try container.decode(String?.self, forKey: .eventName)
let location = try container.decode([Float]?.self, forKey: .location)
let cllocation = CLLocation(latitude: CLLocationDegrees(location[0]), CLLocationDegrees(longitude[1]))
self.init(eventId: eventId, eventName: eventName, location:cllocation)

}

Stephen O'Connor
  • 1,465
  • 1
  • 10
  • 20