Saving custom Swift class with NSCoding to UserDefaults

I am currently trying to save a custom Swift class to NSUserDefaults. Here is the code from my Playground:

import Foundation

class Blog : NSObject, NSCoding {

    var blogName: String?

    override init() {}

    required init(coder aDecoder: NSCoder) {
        if let blogName = aDecoder.decodeObjectForKey("blogName") as? String {
            self.blogName = blogName
        }
    }

    func encodeWithCoder(aCoder: NSCoder) {
        if let blogName = self.blogName {
            aCoder.encodeObject(blogName, forKey: "blogName")
        }
    }

}

var blog = Blog()
blog.blogName = "My Blog"

let ud = NSUserDefaults.standardUserDefaults()    
ud.setObject(blog, forKey: "blog")

When I run the code, I get the following error

Execution was interrupted, reason: signal SIGABRT.

in the last line (ud.setObject...)

The same code also crashes when in an app with the message

"Property list invalid for format: 200 (property lists cannot contain objects of type 'CFType')"

Can anybody help? I am using Xcode 6.0.1 on Maverick. Thanks.


Solution 1:

In Swift 4 or higher, Use Codable.

In your case, use following code.

class Blog: Codable {
   var blogName: String?
}

Now create its object. For example:

var blog = Blog()
blog.blogName = "My Blog"

Now encode it like this:

if let encoded = try? JSONEncoder().encode(blog) {
    UserDefaults.standard.set(encoded, forKey: "blog")
}

and decode it like this:

if let blogData = UserDefaults.standard.data(forKey: "blog"),
    let blog = try? JSONDecoder().decode(Blog.self, from: blogData) {
}

Solution 2:

The first problem is you have to ensure that you have a non-mangled class name:

@objc(Blog)
class Blog : NSObject, NSCoding {

Then you have to encode the object (into an NSData) before you can store it into the user defaults:

ud.setObject(NSKeyedArchiver.archivedDataWithRootObject(blog), forKey: "blog")

Similarly, to restore the object you'll need to unarchive it:

if let data = ud.objectForKey("blog") as? NSData {
    let unarc = NSKeyedUnarchiver(forReadingWithData: data)
    unarc.setClass(Blog.self, forClassName: "Blog")
    let blog = unarc.decodeObjectForKey("root")
}

Note that if you're not using it in the playground it's a little simpler as you don't have to register the class by hand:

if let data = ud.objectForKey("blog") as? NSData {
    let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data)
}

Solution 3:

As @dan-beaulieu suggested I answer my own question:

Here is the working code now:

Note: Demangling of the class name was not necessary for the code to work in Playgrounds.

import Foundation

class Blog : NSObject, NSCoding {

    var blogName: String?

    override init() {}

    required init(coder aDecoder: NSCoder) {
        if let blogName = aDecoder.decodeObjectForKey("blogName") as? String {
            self.blogName = blogName
        }
    }

    func encodeWithCoder(aCoder: NSCoder) {
        if let blogName = self.blogName {
            aCoder.encodeObject(blogName, forKey: "blogName")
        }
    }

}

let ud = NSUserDefaults.standardUserDefaults()

var blog = Blog()
blog.blogName = "My Blog"

ud.setObject(NSKeyedArchiver.archivedDataWithRootObject(blog), forKey: "blog")

if let data = ud.objectForKey("blog") as? NSData {
    let unarc = NSKeyedUnarchiver(forReadingWithData: data)
    let newBlog = unarc.decodeObjectForKey("root") as Blog
}

Solution 4:

Here is a complete solution for Swift 4 & 5.

First, implement helper methods in UserDefaults extension:

extension UserDefaults {

    func set<T: Encodable>(encodable: T, forKey key: String) {
        if let data = try? JSONEncoder().encode(encodable) {
            set(data, forKey: key)
        }
    }

    func value<T: Decodable>(_ type: T.Type, forKey key: String) -> T? {
        if let data = object(forKey: key) as? Data,
            let value = try? JSONDecoder().decode(type, from: data) {
            return value
        }
        return nil
    }
}

Say, we want to save and load a custom object Dummy with 2 default fields. Dummy must conform to Codable:

struct Dummy: Codable {
    let value1 = "V1"
    let value2 = "V2"
}

// Save
UserDefaults.standard.set(encodable: Dummy(), forKey: "K1")

// Load
let dummy = UserDefaults.standard.value(Dummy.self, forKey: "K1")

Solution 5:

Tested with Swift 2.1 & Xcode 7.1.1

If you don't need blogName to be an optional (which I think you don't), I would recommend a slightly different implementation :

class Blog : NSObject, NSCoding {

    var blogName: String

    // designated initializer
    //
    // ensures you'll never create a Blog object without giving it a name
    // unless you would need that for some reason?
    //
    // also : I would not override the init method of NSObject

    init(blogName: String) {
        self.blogName = blogName

        super.init()        // call NSObject's init method
    }

    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(blogName, forKey: "blogName")
    }

    required convenience init?(coder aDecoder: NSCoder) {
        // decoding could fail, for example when no Blog was saved before calling decode
        guard let unarchivedBlogName = aDecoder.decodeObjectForKey("blogName") as? String
            else {
                // option 1 : return an default Blog
                self.init(blogName: "unnamed")
                return

                // option 2 : return nil, and handle the error at higher level
        }

        // convenience init must call the designated init
        self.init(blogName: unarchivedBlogName)
    }
}

test code could look like this :

    let blog = Blog(blogName: "My Blog")

    // save
    let ud = NSUserDefaults.standardUserDefaults()
    ud.setObject(NSKeyedArchiver.archivedDataWithRootObject(blog), forKey: "blog")
    ud.synchronize()

    // restore
    guard let decodedNSData = ud.objectForKey("blog") as? NSData,
    let someBlog = NSKeyedUnarchiver.unarchiveObjectWithData(decodedNSData) as? Blog
        else {
            print("Failed")
            return
    }

    print("loaded blog with name : \(someBlog.blogName)")

Finally, I'd like to point out that it would be easier to use NSKeyedArchiver and save your array of custom objects to a file directly, instead of using NSUserDefaults. You can find more about their differences in my answer here.