Search code examples
swiftobjectcodablejsonencoder

App crashes when trying to encode custom objects that conform to codable protocol and I can't figure out why, any thoughts?


So I have two custom objects, Exercises and Workouts. Exercises conforms to Codable and is able to be encoded and decoded just fine using JSONEncoder. I have an array of Exercise objects that I archive. The issue is with the Workout object. For some reason it does not get encoded using the JSON encoder. I am trying to encode an object which contains two arrays of Workout objects.

Any thoughts as to why this is crashing? I earlier had the manual implementation of Codable in Workout object but it would crash at this line:

let exercisesData = try NSKeyedArchiver.archivedData(withRootObject: exercises, requiringSecureCoding: true)

Then I realized all attributes of Workout already conform to Codable so I commented it out. Code is below.

With the manual implementation of Codable in the Workout object removed, the error message I get is:

The data couldn't be written because it isn't in the correct format

(again I'm using JSONEncoder to encode an object with 2 arrays of Workout objects). If I leave the manual implementation uncommented, it crashes at this line:

let exercisesData = try NSKeyedArchiver.archivedData(withRootObject: exercises, requiringSecureCoding: true)

with the message:

Exercise does not implement methodSignatureForSelector -- trouble ahead Unrecognized selector Exercise replacementObjectForKeyedArchiver

I'm new to Swift programming and I was following some guides on how to set this up. It has worked for me a few weeks ago with simpler data.

class Exercise: Equatable, Codable, NSCopying {
    var name: String
    var muscleGroup: String
    var description: String
    var image: UIImage
    var logs = [(weight: String, reps: String, complete: Bool)]()
    private let isPrimary: Bool
    
    //Init function
    init(name: String, muscleGroup: String, description: String, image: UIImage, isPrimary: Bool = false) {
        self.name = name //Treat this as unique
        self.muscleGroup = muscleGroup
        self.description = description
        self.image = image
        self.isPrimary = isPrimary
    }
    
    //Override equatable to check for uniqueness
    static func == (lhs: Exercise, rhs: Exercise) -> Bool {
        return lhs.name.lowercased() == rhs.name.lowercased()
    }
    
    public enum CodingKeys: String, CodingKey {
        case name
        case muscleGroup
        case description
        case image
        case logs
        case isPrimary
    }
    
    //Decoder function implementation
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        // Decoding the name property
        let nameData = try container.decode(Data.self, forKey: .name)
        self.name = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(nameData) as? String ?? "No Exercise Name"

        // Decoding the muscleGroup property
        let muscleGroupData = try container.decode(Data.self, forKey: .muscleGroup)
        self.muscleGroup = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(muscleGroupData) as? String ?? "Other"

        // Decoding the description property
        let descriptionData = try container.decode(Data.self, forKey: .description)
        self.description = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(descriptionData) as? String ?? "None"
        
        // Decoding the image property
        let imageData = try container.decode(Data.self, forKey: .image)
        self.image = try (NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(imageData) as? UIImage)!
        
        // Deocding the logs property
        let logsData = try container.decode(Data.self, forKey: .logs)
        self.logs = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(logsData) as? [(weight: String, reps: String, complete: Bool)] ?? [(weight: String, reps: String, complete: Bool)]()
        
        // Deocding the primary property
        let isPrimaryData = try container.decode(Data.self, forKey: .isPrimary)
        self.isPrimary = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(isPrimaryData) as? Bool ?? false
    }

    //Encoder function implementation
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        // Encoding the name property
        let nameData = try NSKeyedArchiver.archivedData(withRootObject: name, requiringSecureCoding: true)
        try container.encode(nameData, forKey: .name)

        // Encoding the muscleGroup property
        let muscleGroupData = try NSKeyedArchiver.archivedData(withRootObject: muscleGroup, requiringSecureCoding: true)
        try container.encode(muscleGroupData, forKey: .muscleGroup)

        // Encoding the description property
        let descriptionData = try NSKeyedArchiver.archivedData(withRootObject: description, requiringSecureCoding: true)
        try container.encode(descriptionData, forKey: .description)
        
        // Encoding the image property
        let imageData = try NSKeyedArchiver.archivedData(withRootObject: image, requiringSecureCoding: true)
        try container.encode(imageData, forKey: .image)
        
        // Encoding the logs property
        let logsData = try NSKeyedArchiver.archivedData(withRootObject: logs, requiringSecureCoding: true)
        try container.encode(logsData, forKey: .logs)
        
        // Encoding the primary property
        let isPrimaryData = try NSKeyedArchiver.archivedData(withRootObject: isPrimary, requiringSecureCoding: true)
        try container.encode(isPrimaryData, forKey: .isPrimary)
     }
}
class Workout: Codable {
    var title: String
    var date: Date
    let duration: String //in minutes
    var exercises = [Exercise]()
    private let isPrimary: Bool
    
    //Init function
    init(title: String, date: Date, duration: String, exercises: [Exercise], isPrimary: Bool = false) {
        self.title = title
        self.date = date
        self.duration = duration
        self.exercises = exercises
        self.isPrimary = isPrimary
    }
    
    // public enum CodingKeys: String, CodingKey {
    //    case title
    //    case date
    //    case duration
    //    case exercises
    //    case isPrimary
    // }

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

    //     // Decoding the title property
    //     let titleData = try container.decode(Data.self, forKey: .title)
    //     self.title = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(titleData) as? String ?? "New Workout"

    //     // Decoding the date property
    //     let dateData = try container.decode(Data.self, forKey: .date)
    //     self.date = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(dateData) as? Date ?? Date()

    //     // Decoding the duration property
    //     let durationData = try container.decode(Data.self, forKey: .duration)
    //     self.duration = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(durationData) as? String ?? "0"

    //     //Decoding the exercises property
    //     let exercisesData = try container.decode(Data.self, forKey: .exercises)
    //     self.exercises = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(exercisesData) as? [Exercise] ?? [Exercise]()

    //     // Deocding the primary property
    //     let isPrimaryData = try container.decode(Data.self, forKey: .isPrimary)
    //     self.isPrimary = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(isPrimaryData) as? Bool ?? false
    // }

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

    //     // Encoding the title property
    //     let titleData = try NSKeyedArchiver.archivedData(withRootObject: title, requiringSecureCoding: true)
    //     try container.encode(titleData, forKey: .title)

    //     // Encoding the date property
    //     let dateData = try NSKeyedArchiver.archivedData(withRootObject: date, requiringSecureCoding: true)
    //     try container.encode(dateData, forKey: .date)

    //     // Encoding the duration property
    //     let durationData = try NSKeyedArchiver.archivedData(withRootObject: duration, requiringSecureCoding: true)
    //     try container.encode(durationData, forKey: .duration)

    //     // Encoding the exercises property
    //     let exercisesData = try NSKeyedArchiver.archivedData(withRootObject: exercises, requiringSecureCoding: true)
    //     try container.encode(exercisesData, forKey: .exercises)

    //     // Encoding the primary property
    //     let isPrimaryData = try NSKeyedArchiver.archivedData(withRootObject: isPrimary, requiringSecureCoding: true)
    //     try container.encode(isPrimaryData, forKey: .isPrimary)
    // }
}

Solution

  • There's no reason at all to be using NSKeyedArchiver in your encode and no reason to use NSKeyedUnarchiver in your init. Just use the APIs provided by Encoder and Decoder.

    Here's your code all cleaned up. I replaced the tuple being used for the logs with another struct. I changed your two classes to be struct. Only the Exercise struct needs a custom init and encode since UIImage doesn't conform to Codable. The only need for the custom init with all of the properties is to support default values for some of the properties.

    Note that if a struct has properties that are all Codable then the whole struct is Codable without writing any code. Same for Equatable.

    struct Log: Codable {
        let weight: String
        let reps: String
        let complete: Bool
    }
    
    struct Exercise: Equatable, Codable {
        let name: String
        let muscleGroup: String
        let description: String
        let image: UIImage
        let logs: [Log]
        let isPrimary: Bool
    
        init(name: String, muscleGroup: String, description: String, image: UIImage, logs: [Log] = [], isPrimary: Bool = false) {
            self.name = name
            self.muscleGroup = muscleGroup
            self.description = description
            self.image = image
            self.logs = logs
            self.isPrimary = isPrimary
        }
    
        //Override equatable to check for uniqueness
        static func == (lhs: Exercise, rhs: Exercise) -> Bool {
            return lhs.name.lowercased() == rhs.name.lowercased()
        }
    
        public enum CodingKeys: String, CodingKey {
            case name
            case muscleGroup
            case description
            case image
            case logs
            case isPrimary
        }
    
        //Decoder function implementation
        init(from decoder: Decoder) throws {
            let container = try decoder.container(keyedBy: CodingKeys.self)
    
            // Decoding the name property
            name = try container.decode(String.self, forKey: .name)
    
            // Decoding the muscleGroup property
            muscleGroup = try container.decode(String.self, forKey: .muscleGroup)
    
            // Decoding the description property
            description = try container.decode(String.self, forKey: .description)
    
            // Decoding the image property
            let imageData = try container.decode(Data.self, forKey: .image)
            image = UIImage(data: imageData)!
    
            // Deocding the logs property
            logs = try container.decode([Log].self, forKey: .logs)
    
            // Deocding the primary property
            isPrimary = try container.decode(Bool.self, forKey: .isPrimary)
        }
    
        //Encoder function implementation
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: CodingKeys.self)
    
            // Encoding the name property
            try container.encode(name, forKey: .name)
    
            // Encoding the muscleGroup property
            try container.encode(muscleGroup, forKey: .muscleGroup)
    
            // Encoding the description property
            try container.encode(description, forKey: .description)
    
            // Encoding the image property
            let imageData = image.pngData()
            try container.encode(imageData, forKey: .image)
    
            // Encoding the logs property
            try container.encode(logs, forKey: .logs)
    
            // Encoding the primary property
            try container.encode(isPrimary, forKey: .isPrimary)
        }
    }
    
    struct Workout: Codable {
        let title: String
        let date: Date
        let duration: String //in minutes
        let exercises: [Exercise]
        let isPrimary: Bool
    
        init(title: String, date: Date, duration: String, exercises: [Exercise], isPrimary: Bool = false) {
            self.title = title
            self.date = date
            self.duration = duration
            self.exercises = exercises
            self.isPrimary = isPrimary
        }
    }