Save struct in class to NSUserDefaults using Swift
Asked Answered
C

4

8

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.

Coccidiosis answered 28/8, 2014 at 10:36 Comment(0)
C
10

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)
    
Clambake answered 28/8, 2014 at 11:35 Comment(1)
let structDicts = mystructs.map { ["start": $0.start, "stop": $0.stop] } -> fatal error: NSArray element failed to match the Swift Array Element typeCoccidiosis
A
6

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.

Aliped answered 28/8, 2014 at 11:31 Comment(0)
R
0

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.

Romaine answered 14/1, 2017 at 2:54 Comment(0)
R
0

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)
}
Rivalry answered 24/8, 2018 at 11:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.