Search code examples
swiftswift5

How can I save an object of a custom class in Userdefaults in swift 5/ Xcode 10.2


I want to save the array patientList in UserDefaults. Patient is an custom class so I need to transfer it into Data object, but this doesn't work on Swift 5 like it did before.

func addFirstPatient(){
    let newPatient = Patient(name: nameField.text!, number: numberField.text!, resultArray: resultArray, diagnoseArray: diagnoseArray)
    let patientList: [Patient] = [newPatient]
    let encodeData: Data = NSKeyedArchiver.archivedData(withRootObject: patientList)
    UserDefaults.standard.set(encodeData, forKey: "patientList")
    UserDefaults.standard.synchronize()
}
struct Patient {
    var diagnoseArray: [Diagnose]
    var resultArray: [Diagnose]
    var name: String
    var number: String
    init(name: String, number: String, resultArray: [Diagnose], diagnoseArray: [Diagnose]) {
        self.diagnoseArray = diagnoseArray
        self.name = name
        self.number = number
        self.resultArray = resultArray
    }
}
struct Diagnose{
    var name: String
    var treatments: [Treatment]
    var isPositiv = false
    var isExtended = false
    init(name: String, treatments: [Treatment]) {
        self.name = name
        self.treatments = treatments
    }
}
struct Treatment {
    var name: String
    var wasMade = false
    init(name: String) {
        self.name = name
    }
}

This is what the function looks like. The problem is in the line where I initialize encodeData.

let encodeData: Data = try! NSKeyedArchiver.archivedData(withRootObject: patientList, requiringSecureCoding: false)

This is what Swift suggests but when I try it like this it always crashes and I don't get the error


Solution

  • Vadian's answer is correct, you cannot use NSKeyedArchiver with structs. Having all your objects conform to Codable is the best way to reproduce the behavior you are looking for. I do what Vadian does, but I you can also use protocol extensions to make this safer.

    import UIKit
    
    struct Patient: Codable {
        var name: String
        var number: String
        var resultArray: [Diagnose]
        var diagnoseArray: [Diagnose]
    }
    
    struct Diagnose: Codable {
        var name: String
        var treatments: [Treatment]
        var isPositiv : Bool
        var isExtended : Bool
    }
    
    struct Treatment: Codable {
        var name: String
        var wasMade : Bool
    }
    
    let newPatient = Patient(name: "John Doe",
                             number: "123",
                             resultArray: [Diagnose(name: "Result", treatments: [Treatment(name: "Treat1", wasMade: false)], isPositiv: false, isExtended: false)],
                             diagnoseArray: [Diagnose(name: "Diagnose", treatments: [Treatment(name: "Treat2", wasMade: false)], isPositiv: false, isExtended: false)])
    let patientList: [Patient] = [newPatient]
    

    Introduce a protocol to manage the encoding and saving of objects.

    This does not have to inherit from Codable but it does for this example for simplicity.

    /// Objects conforming to `CanSaveToDisk` have a save method and provide keys for saving individual objects or a list of objects.
    protocol CanSaveToDisk: Codable {
    
        /// Provide default logic for encoding this value.
        static var defaultEncoder: JSONEncoder { get }
    
        /// This key is used to save the individual object to disk. This works best by using a unique identifier.
        var storageKeyForObject: String { get }
    
        /// This key is used to save a list of these objects to disk. Any array of items conforming to `CanSaveToDisk` has the option to save as well.
        static var storageKeyForListofObjects: String { get }
    
        /// Persists the object to disk.
        ///
        /// - Throws: useful to throw an error from an encoder or a custom error if you use stage different from user defaults like the keychain
        func save() throws
    
    }
    

    Using protocol extensions we add an option to save an array of these objects.

    extension Array where Element: CanSaveToDisk {
    
        func dataValue() throws -> Data {
            return try Element.defaultEncoder.encode(self)
        }
    
        func save() throws {
            let storage = UserDefaults.standard
            storage.set(try dataValue(), forKey: Element.storageKeyForListofObjects)
        }
    
    }
    
    

    We extend our patient object so it can know what to do when saving.

    I use "storage" so that this could be swapped with NSKeychain. If you are saving sensitive data (like patient information) you should be using the keychain instead of UserDefaults. Also, make sure you comply with security and privacy best practices for health data in whatever market you're offering your app. Laws can be a very different experience between countries. UserDefaults might not be safe enough storage.

    There are lots of great keychain wrappers to make things easier. UserDefaults simply sets data using a key. The Keychain does the same. A wrapper like https://github.com/evgenyneu/keychain-swift will behave similar to how I use UserDefaults below. I have commented out what the equivalent use would look like for completeness.

    extension Patient: CanSaveToDisk {
    
        static var defaultEncoder: JSONEncoder {
            let encoder = JSONEncoder()
            // add additional customization here
            // like dates or data handling
            return encoder
        }
    
        var storageKeyForObject: String {
            // "com.myapp.patient.123"
            return "com.myapp.patient.\(number)"
        }
    
        static var storageKeyForListofObjects: String {
            return "com.myapp.patientList"
        }
    
        func save() throws {
    
            // you could also save to the keychain easily
            //let keychain = KeychainSwift()
            //keychain.set(dataObject, forKey: storageKeyForObject)
    
            let data = try Patient.defaultEncoder.encode(self)
            let storage = UserDefaults.standard
            storage.setValue(data, forKey: storageKeyForObject)
        }
    }
    

    Saving is simplified, check out the 2 examples below!

    do {
    
        // saving just one patient record
        // this saves this patient to the storageKeyForObject
        try patientList.first?.save()
    
        // saving the entire list
        try patientList.save()
    
    
    } catch { print(error) }