Custom class conformance to MKAnnotation and Codable

428 views Asked by At

I'm working on a group project application in Swift for iOS that allows users to save Location(s) as part of a Tour using Firebase. Our User object is a custom class that we hope to conform to Codable for use with the CodableFirebase CocoaPod. However, the issue arises when trying to conform the Location object to Codable because it also has to conform to MKAnnotation... and we are all fairly new to this. It's possible that some solutions have gone over my head... entirely possible.

This is our Location object:

import Foundation
import MapKit
import CoreLocation
import CodableFirebase

class Location: NSObject, MKAnnotation {

    var coordinate: CLLocationCoordinate2D
    var locationName: String?
    var locationDescription: String?
    var locationImage: UIImage?

    init(coordinate: CLLocationCoordinate2D, locationName: String, locationDescription: String, locationImage: UIImage) {

        self.coordinate = coordinate
        self.locationName = locationName
        self.locationDescription = locationDescription
        self.locationImage = locationImage

    }
}

And this is our User model object:

import UIKit
import CloudKit

class User {

    //MARK: - Editable Public Profile Info
    var firstName: String
    var lastName: String
    var userStateOfOrigin: States
    var userImage: UIImage?
    var aboutMe: String
    var languages: [Language]

    //MARK: - Editable Personal Info (Includes all Public Profile info)
    var email: String?
    var phoneNumber: String?

    //MARK: - Hidden Variables
    var userId: String
    var userCreatedTours: [Tour]
    var savedLocations: [Location]
    var savedTours: [Tour]
    var bookedTours: [Tour]
    var previouslyExperiencedTours: [Tour]
    var userRating: [Rating]

    init(firstName: String, lastName: String, userStateOfOrigin: States, userImage: UIImage? = nil, aboutMe: String, languages: [Language] = [], email: String?, phoneNumber: String? = "", userId: String, userCreatedTours: [Tour] = [], savedLocations: [Location] = [], savedTours: [Tour] = [], bookedTours: [Tour] = [], previouslyExperiencedTours: [Tour] = [], userRating: [Rating] = []) {

        // editable Public info
        self.firstName =  firstName
        self.lastName = lastName
        self.userStateOfOrigin = userStateOfOrigin
        self.userImage = userImage
        self.aboutMe = aboutMe
        self.languages = languages

        // editable personal info
        self.email = email
        self.phoneNumber = phoneNumber

        // Hidden Variables
        self.userId = userId
        self.userCreatedTours = userCreatedTours
        self.savedLocations = savedLocations
        self.savedTours = savedTours
        self.bookedTours = bookedTours
        self.previouslyExperiencedTours = previouslyExperiencedTours
        self.userRating = userRating

    }
}

We do have a few other custom objects, but I think once this issue is solved, we should be able to easily conform those to Codable. Ultimately, it would be great to be able to make these both conform to Codable for easy use with Firebase. However, if that's not possible, or the most prudent route, I'm very open to suggestions. I really appreciate it.

1

There are 1 answers

0
Rob On

A couple of observations:

  1. MKAnnotation protocol has title and subtitle properties (that many annotation views use). I might suggest renaming locationName and locationDescription, respectively.

  2. I’d suggest making these properties dynamic, that way if you ever update them, the changes will be updated on their respective annotation views.

    For example, the documentation for coordinate says:

    Your implementation of this property must be key-value observing (KVO) compliant. For more information on how to implement support for KVO, see Key-Value Observing Programming Guide.

    The easy way to do this in Swift is with the dynamic keyword.

  3. To make this Codable, either:

    • the properties must be Codable, too; or

    • you will need to manually implement init(from:) and encode(to:) to do the decoding and encoding, respectively. See Encoding and Decoding Custom Types for examples.
       

    Given that neither CLLocationCoordinate2D nor UIImage conform to Codable, I’d lean towards this second option.

  4. A very minor point, but I might not use a type name of Location, but rather using something with Annotation in the name, to make it clear what the object is. It also helps avoid confusion with location objects, e.g. CLLocation. I used CustomAnnotation below, which isn’t a great name, but hopefully you can come up with a name that makes more sense within your app, but also has Annotation within the name.

Thus, pulling this all together you might do something like:

class CustomAnnotation: NSObject, MKAnnotation, Codable {
    dynamic var coordinate: CLLocationCoordinate2D
    dynamic var title: String?
    dynamic var subtitle: String?
    dynamic var image: UIImage?

    init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?, locationImage: UIImage?) {
        self.coordinate = coordinate
        self.title = title
        self.subtitle = subtitle
        self.image = locationImage
    }

    enum CodingKeys: String, CodingKey {
        case title, subtitle, image, latitude, longitude
    }

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

        title = try values.decodeIfPresent(String.self, forKey: .title)
        subtitle = try values.decodeIfPresent(String.self, forKey: .subtitle)

        let latitude = try values.decode(CLLocationDegrees.self, forKey: .latitude)
        let longitude = try values.decode(CLLocationDegrees.self, forKey: .longitude)
        coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)

        let data = try values.decodeIfPresent(Data.self, forKey: .image)
        image = data.flatMap { UIImage(data: $0) }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        try container.encodeIfPresent(title, forKey: .title)
        try container.encodeIfPresent(subtitle, forKey: .subtitle)
        try container.encode(coordinate.latitude, forKey: .latitude)
        try container.encode(coordinate.longitude, forKey: .longitude)
        try container.encodeIfPresent(image?.pngData(), forKey: .image)
    }
}