简体   繁体   中英

Save struct in class to NSUserDefaults using Swift

I have a class and inside the class is a (swift) array, based on a global struct. I want to save an array with this class to NSUserDefaults. This is my code:

struct mystruct {
    var start : NSDate = NSDate()
    var stop : NSDate = NSDate()
}

class MyClass : NSObject {

    var mystructs : [mystruct]

    init(mystructs : [mystruct]) {

        self.mystructs = mystructs 
        super.init()
    }

    func encodeWithCoder(encoder: NSCoder) {
        //let val = mystructs.map { $0 as NSObject } //this also doesn't work
        let objctvtmrec = NSMutableArray(mystructs)  //gives error
        encoder.encodeObject(objctvtmrec)
        //first approach:
        encoder.encodeObject(mystructs) //error: [mystructs] doesn't conform to protocol 'anyobject'
    }

}

var records : [MyClass] {
    get {
        var returnValue : [MyClass]? = NSUserDefaults.standardUserDefaults().objectForKey("records") as? [MyClass]
        if returnValue == nil
        {
            returnValue = []
        }
        return returnValue!
    }
    set (newValue) {
        let val = newValue.map { $0 as AnyObject }
        NSUserDefaults.standardUserDefaults().setObject(val, forKey: "records")
        NSUserDefaults.standardUserDefaults().synchronize()
    }
}

I already subclassed to NSObject, and I know I need NSCoding. But I don't find any way to convert the struct array to an NSMuteableArray or something similar I can store. The only idea until now is to go through each entry and copy it directly to a new array or to use much or objective-c code all over the project, so i never need to convert from swift arrays to objective-c arrays. Both are things I don't want to do.

Swift structs are not classes , therefore they don't conform to AnyObject protocol. You have to rethink your approach. Here are some suggestions:

  1. Convert your struct to final class to enforce immutability

     final class MyStruct { let start : NSDate = NSDate() let stop : NSDate = NSDate() } encoder.encodeObject(mystructs) 
  2. Map them as an array dictionaries of type [String: NSDate]

     let structDicts = mystructs.map { ["start": $0.start, "stop": $0.stop] } encoder.encodeObject(structDicts) 

NSUserDefaults is limited in the types it can handle: NSData , NSString , NSNumber , NSDate , NSArray , NSDictionary , and Bool . Thus no Swift objects or structs can be saved. Anything else must be converted to an NSData object.

NSUserDefaults does not work the same way as NSArchiver . Since you already have added NSCoder to your classes your best choice might be to save and restore with NSArchiver to a file in the Documents directory..

From the Apple NSUserDefaults Docs:

A default object must be a property list, that is, an instance of (or for collections a combination of instances of): NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. If you want to store any other type of object, you should typically archive it to create an instance of NSData.

I've developed a small library which may help. You can use it as a replacement of NSCoding for Swift structs.

You would need to implement a Koting protocol for mystruct :

struct mystruct: Koting {

    var start : NSDate = NSDate()
    var stop : NSDate = NSDate()

    // MARK: - Koting

    init?(koter: Koter) {
        guard let start: NSDate = koter.dekotObject(forKey: "start"),
              let stop: NSDate = koter.dekotObject(forKey: "stop") else {
            return nil
        }
        self.init(start: start, stop: stop)
    }

    func enkot(with koter: Koter) {
        koter.enkotObject(start, forKey: "start")
        koter.enkotObject(stop, forKey: "stop")
    }
}

Since then you may easily convert the struct to Data and back:

let str = mystruct(start: NSDate(/*...*/), stop: NSDate(/*...*/))
guard let data = str.de_data else { return }  // potentially may be nil
let restoredStr = mystruct.de_from(data: data)   // if data is irrelevant, returns `nil`

Finally, this is what you do to implement NSCoding :

class MyClass: NSObject, NSCoding {

    var mystructs: [mystruct]

    init(mystructs: [mystruct]) {

        self.mystructs = mystructs 
        super.init()
    }

    func encode(with aCoder: NSCoder) {
        guard let datas = mystructs.flatMap { $0.de_data } else { return }
        aCoder.encode(datas, forKey: "mystructs")
    }

    required convenience init?(coder aDecoder: NSCoder) {
        guard let datas = aDecoder.decodeObject(forKey: "mystructs") as? [Data],
              let mystructs = datas.flatMap { mystruct.de_from(data: $0) } else {
            return nil
        }

        self.init(mystructs : mystructs)
    }
}

It's pretty much the same code you would write if NSCoding supported Swift structs.

I use this this in my project while coding with Swift 4:

let jsonData = """ {"variable1":1234,"variable2":"someString"}"""

struct MyStruct:Codable{ var variable1 :Int var variable2:String }

let whatever = try JSONDecoder().decode(MyStruct.self,from:jsonData)

let encoded =try JSONEncoder().encode(whatever)

UserDefaults.standart.set(encoded, forKey:"encodedData")

to fetch data from UserDefaults

if let data = UserDefaults.standard.object(forKey: "encodedData") as? Data {
    let myStruct = try JSONDecoder().decode(MyStruct.self, from: data)
    print(myStruct)
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM