问题
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