Cannot store custom class in UserDefaults - Attempt to insert non-property list

↘锁芯ラ 提交于 2021-02-11 13:04:17

问题


I'm trying to store a custom class in UserDefaults, however I'm getting the "Attempt to insert non-property list" error as soon as the app starts. I have successfully stored arrays of custom classes, so I followed the same principle, but something is wrong.

This is my class:

import Foundation

public class PlanRouteFilters: NSObject, NSCoding, Codable {
    
    var favoriteLines: Bool
    var routeType: Int
    var bus: Bool
    var train: Bool
    var subway: Bool
    var electric: Bool
    var sharedCar: Bool
    var bike: Bool
    var electricScooter: Bool
    var providers: [Provider]?
    
    override init() {
        self.favoriteLines = false
        self.routeType = 1
        self.bus = false
        self.train = false
        self.subway = false
        self.electric = false
        self.sharedCar = false
        self.bike = false
        self.electricScooter = false
        self.providers = []
    }
    
    init(favLines: Bool, rType: Int, bus: Bool, train: Bool, subway: Bool, electric: Bool, sCar: Bool, bike: Bool, eScooter: Bool, providers: [Provider]?) {
        self.favoriteLines = favLines
        self.routeType = rType
        self.bus = bus
        self.train = train
        self.subway = subway
        self.electric = electric
        self.sharedCar = sCar
        self.bike = bike
        self.electricScooter = eScooter
        self.providers = providers
        
    }
    
    //MARK: NSCoding protocol methods
    public func encode(with aCoder: NSCoder){
        aCoder.encode(self.favoriteLines, forKey: "favoriteLines")
        aCoder.encode(self.routeType, forKey: "RouteType")
        aCoder.encode(self.bus, forKey: "bus")
        aCoder.encode(self.train, forKey: "train")
        aCoder.encode(self.subway, forKey: "subway")
        aCoder.encode(self.electric, forKey: "electric")
        aCoder.encode(self.sharedCar, forKey: "sharedCar")
        aCoder.encode(self.bike, forKey: "bike")
        aCoder.encode(self.electricScooter, forKey: "electricScooter")
        aCoder.encode(self.providers, forKey: "providers")
    }
    
    public required init(coder decoder: NSCoder) {
        self.favoriteLines = decoder.decodeBool(forKey: "favoriteLines")
        self.routeType = decoder.decodeInteger(forKey: "routeType")
        self.bus = decoder.decodeBool(forKey: "bus")
        self.train = decoder.decodeBool(forKey: "train")
        self.subway = decoder.decodeBool(forKey: "subway")
        self.electric = decoder.decodeBool(forKey: "electric")
        self.sharedCar = decoder.decodeBool(forKey: "sharedCar")
        self.bike = decoder.decodeBool(forKey: "bike")
        self.electricScooter = decoder.decodeBool(forKey: "electricScooter")
        self.providers = decoder.decodeObject(forKey: "providers") as? [Provider]
    }
}

This is how I set and get the object:

var planRouteFilters: PlanRouteFilters {
        get {
            var filters = PlanRouteFilters()
            let data = self.userSettings.settings.object(forKey: "planRouteFilters") as? Data
            if nil != data {
                filters = try! NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data!) as! PlanRouteFilters
            }
            return filters
        }
        set {
            let data = try! NSKeyedArchiver.archivedData(withRootObject: newValue, requiringSecureCoding: false)
            self.userSettings.settings.setValue(data, forKey: "planRouteFilters")
            self.objectWillChange.send()
        }
    }

And this is how I create the userDefaults:

import Foundation

struct UserSettings {
    var settings: UserDefaults
    
    init() {
        self.settings = UserDefaults.standard
        self.settings.register(
            defaults: [
                "userCity": "",
                "showUserCityActionSheet": true,
                "showTutorial": 1,
                "initialPage": 1,
                "favoriteStops": [Place](),
                "history": [Place](), //Maximum of 10 places stored
                "notifications": [MoveMeNotification](),
                "planRouteFilters": PlanRouteFilters()
            ])
    }
}

If I remove the default value for the PlanRouteFilters the app starts, but the values are not used throughout the app. Is this where I am wrong? Should I provide the default value in some other way?

Thanks!

My Provider class:

public class Provider: NSObject, NSCoding, Codable {

    let name: String

    init(name: String) {
      self.Name = name
    }

    public func encode(with aCoder: NSCoder) {
      aCoder.encode(self.Name, forKey: "Name")
    }

    public required init(coder decoder: NSCoder) {
      self.Name = decoder.decodeObject(forKey: "Name") as! String
    }
}

UPDATE:

Ok, I was not able to solve this issue. However, I figured I did not need to have a default value in the userSettings. That allows me to move on. However, that is specific to my case. For any of those that don't have this option, take a look at @LeoDabus answers.


回答1:


Even when conforming your custom class to NSCoding it doesn't work automatically. You need to encode your providers array into Data to be able to encode PlanRouteFilters:

extension NSCoder {
    func decodeData(forKey key: String) -> Data {
        decodeObject(forKey: key) as? Data ?? Data()
    }
    func decodeString(forKey key: String) -> String {
        decodeObject(forKey: key) as? String ?? ""
    }
}

public class Provider: NSObject, NSCoding {
    let name: String
    init(name: String) { self.name = name }
    public func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
    }
    public required init(coder decoder: NSCoder) {
        name = decoder.decodeString(forKey: "name")
    }
}

public class PlanRouteFilters: NSObject, NSCoding {
    var favoriteLines: Bool
    var routeType: Int
    var bus: Bool
    var train: Bool
    var subway: Bool
    var electric: Bool
    var sharedCar: Bool
    var bike: Bool
    var electricScooter: Bool
    fileprivate var providersData = Data()
    var providers: [Provider] = []
    override init() {
        self.favoriteLines = false
        self.routeType = 1
        self.bus = false
        self.train = false
        self.subway = false
        self.electric = false
        self.sharedCar = false
        self.bike = false
        self.electricScooter = false
        self.providers = []
    }
    init(favLines: Bool, rType: Int, bus: Bool, train: Bool, subway: Bool, electric: Bool, sCar: Bool, bike: Bool, eScooter: Bool, providers: [Provider]) {
        self.favoriteLines = favLines
        self.routeType = rType
        self.bus = bus
        self.train = train
        self.subway = subway
        self.electric = electric
        self.sharedCar = sCar
        self.bike = bike
        self.electricScooter = eScooter
        self.providers = providers
    }
    public func encode(with aCoder: NSCoder){
        aCoder.encode(favoriteLines, forKey: "favoriteLines")
        aCoder.encode(routeType, forKey: "routeType")
        aCoder.encode(bus, forKey: "bus")
        aCoder.encode(train, forKey: "train")
        aCoder.encode(subway, forKey: "subway")
        aCoder.encode(electric, forKey: "electric")
        aCoder.encode(sharedCar, forKey: "sharedCar")
        aCoder.encode(bike, forKey: "bike")
        aCoder.encode(electricScooter, forKey: "electricScooter")
        aCoder.encode((try? NSKeyedArchiver.archivedData(withRootObject: providers, requiringSecureCoding: false)) ?? Data(), forKey: "providersData")
    }
    
    public required init(coder decoder: NSCoder) {
        favoriteLines = decoder.decodeBool(forKey: "favoriteLines")
        routeType = decoder.decodeInteger(forKey: "routeType")
        bus = decoder.decodeBool(forKey: "bus")
        train = decoder.decodeBool(forKey: "train")
        subway = decoder.decodeBool(forKey: "subway")
        electric = decoder.decodeBool(forKey: "electric")
        sharedCar = decoder.decodeBool(forKey: "sharedCar")
        bike = decoder.decodeBool(forKey: "bike")
        electricScooter = decoder.decodeBool(forKey: "electricScooter")
        providers = (try? NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(decoder.decodeData(forKey: "providersData"))) as? [Provider] ?? []
    }
}

let planRouteFilters: PlanRouteFilters = .init(favLines: true, rType: 1, bus: false, train: true, subway: false, electric: true, sCar: true, bike: true, eScooter: false, providers: [.init(name: "test"), .init(name: "drive")])
do {
    let data = try NSKeyedArchiver.archivedData(withRootObject: planRouteFilters, requiringSecureCoding: false)
    if let loadedFilters = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? PlanRouteFilters {
        print("success", loadedFilters)
        print("favoriteLines", loadedFilters.favoriteLines)
        print("routeType", loadedFilters.routeType ?? "nil")
        print("bus", loadedFilters.bus)
        print("train", loadedFilters.train)
        print("subway", loadedFilters.subway)
        print("electric", loadedFilters.electric)
        print("sharedCar", loadedFilters.sharedCar)
        print("bike", loadedFilters.bike)
        print("electricScooter", loadedFilters.electricScooter)
        print("providers first", loadedFilters.providers.first?.name ?? "nil")
        print("providers last", loadedFilters.providers.last?.name ?? "nil")
    }
} catch {
    print(error)
}

This will print

success <__lldb_expr_54.PlanRouteFilters: 0x600000a410c0>
favoriteLines true
routeType 1
bus false
train true
subway false
electric true
sharedCar true
bike true
electricScooter false
providers first test
providers last drive




A better/easier approach is to stick with Codable protocol and extend UserDefaults to be able to encode/decode Codable types as shown in this answer:

extension UserDefaults {
    func decode<T: Decodable>(_ type: T.Type, forKey defaultName: String) throws -> T {
        try JSONDecoder().decode(T.self, from: data(forKey: defaultName) ?? .init())
    }
    func encode<T: Encodable>(_ value: T, forKey defaultName: String) throws {
        try set(JSONEncoder().encode(value), forKey: defaultName)
    }
}



回答2:


Use PropertyListDecoder() with Codable

Here is an example from my old project. It allows you to save an object, retrieve it and remove it from UserDefaults. If you are using a custom object as a property you need to confirm it to Codable and you may have to use PropertyListDecoder() if necessary. Hope this will helps you to solve the original problem.

class User: Codable {
    var name:String?
    var username: String?
    var password: String?
    var email: String?
    

    
    func saveUserData() {
        if let encoded = try? PropertyListEncoder().encode(self) {
            UserDefaults.standard.set(, forKey: "userObject")
        }
     }

    static var currentUser:User? {
        if let userData = UserDefaults.standard.value(forKey: "userObject") as? Data, let user = try? PropertyListDecoder().decode(User.self, from: userData) {
            return user
        }
        return nil
    }

    static func logout() {
        UserDefaults.standard.removeObject(forKey: "userObject")
 

       }
}



    // To save user object
    myUserObject.saveUserData()
    
    // To retrieve 
    let user = User.currentUser
    
    // To remove 
    User.logout()


来源:https://stackoverflow.com/questions/64664186/cannot-store-custom-class-in-userdefaults-attempt-to-insert-non-property-list

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!