Saving custom Swift class with NSCoding to UserDefaults
Asked Answered
S

12

87

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.

Stavros answered 20/10, 2014 at 15:33 Comment(1)
There is a good tutorial here: ios8programminginswift.wordpress.com/2014/08/17/… A good reddit discussion here: reddit.com/r/swift/comments/28sp3z/… Another good post here: myswiftjourney.me/2014/10/01/… For full object storing I recommend the full NSCoding - two example below, the second one from me with full class structures: #26233567 stackoverflOniskey
S
21

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
}
Stavros answered 30/10, 2015 at 16:52 Comment(1)
What if my custom class has type UIImage or Data? How can i store that kind of class in UserDefaults?Brenn
M
73

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) {
}
Marney answered 27/3, 2018 at 19:40 Comment(4)
If i want to store it permanently then how it should work. Thanks in advanceLiborio
I tried this but, i'm getting the error message, Type Blog does not conform to protocol DecodableVernita
@Vernita did you inherit class Blog from Codable?Marney
how to do it if i have a dictionary [String:Any] in my Blog class ?Jayson
A
46

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)
}
Aubade answered 20/10, 2014 at 16:52 Comment(3)
Thanks. That did it. The NSKeyedUnarchiver was the missing piece. I'll update my question with a working sample. It seems that you can omit the unarc.setClass and instead downcast the unarc.decodeObjectForKey to - in this case - the Blog class.Stavros
You only need the setClass if you're working in the Playground, for various reasons classes don't get automatically registered there.Aubade
the blog variable above is not the custom object in if let data = ud.objectForKey("blog") as? NSData { let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data) }, in my case i need to cast it to my custom object like this if let data = ud.objectForKey("blog") as? NSData { let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data) if let thisblog = blog as? Blog { return thisblog //this is the custom object from nsuserdefaults } }Longitudinal
S
21

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
}
Stavros answered 30/10, 2015 at 16:52 Comment(1)
What if my custom class has type UIImage or Data? How can i store that kind of class in UserDefaults?Brenn
C
14

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")

Conciliator answered 30/1, 2019 at 15:4 Comment(2)
This will not store permanently the data. I want to store data after application killed.Liborio
Why would this not permanently store the data? @LiborioMurrell
A
9

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.

Alive answered 10/1, 2016 at 21:1 Comment(1)
Just FYI: The reason for blogName being an optional is that the object mirrors actual blogs that a user has. The blog name is automatically derived from the title tag once the user adds a URL for a new blog. So at object creation time there is no blog name and I wanted to avoid calling a new blog "unnamed" or something similar.Stavros
R
7

In Swift 4 you have a new protocol that replaces the NSCoding protocol. It's called Codable and it supports classes and Swift types! (Enum, structs):

struct CustomStruct: Codable {
    let name: String
    let isActive: Bool
}
Recruitment answered 7/6, 2017 at 15:12 Comment(2)
class implements Codable and has an optional array of strings: 'Attempt to insert non-property list objectDrumhead
This answer is completely wrong. Codable does not replace NSCoding. The only way to directly save objects to UserDefaults is to make the class NSCoding compliant. Since NSCoding is a class protocol, it cannot be applied to struct/enum types. However, you can still make your struct/enum Coding compliant and use JSONSerializer to encode to Data, which can be stored in UserDefaults.Electromyography
G
4

Swift 3 version:

class CustomClass: NSObject, NSCoding {

    let name: String
    let isActive: Bool

    init(name: String, isActive: Bool) {
        self.name = name
        self.isActive = isActive
    }

    // MARK: NSCoding

    required convenience init?(coder decoder: NSCoder) {
        guard let name = decoder.decodeObject(forKey: "name") as? String,
            let isActive = decoder.decodeObject(forKey: "isActive") as? Bool
            else { return nil }

        self.init(name: name, isActive: isActive)
    }

    func encode(with coder: NSCoder) {
        coder.encode(self.name, forKey: "name")
        coder.encode(self.isActive, forKey: "isActive")
    }
}
Gastrocnemius answered 10/3, 2017 at 20:19 Comment(0)
T
1

I use my own struct. It's much easier.

struct UserDefaults {
    private static let kUserInfo = "kUserInformation"

    var UserInformation: DataUserInformation? {
        get {
            guard let user = NSUserDefaults.standardUserDefaults().objectForKey(UserDefaults.kUserInfo) as? DataUserInformation else {
                return nil
            }
            return user
        }
        set {

            NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: UserDefaults.kUserInfo)
        }
    }
}

To use:

let userinfo = UserDefaults.UserInformation
Transmutation answered 15/1, 2016 at 13:39 Comment(0)
F
1

A simple example of saving Custom Objects in UserDefaults would be as below:

You do not need to write the boilerplate code for saving/retrieving objects any more with the help of THE GREAT 'CODABLE', that's what it is there for you to rescue from irritating manual encoding/decoding.

So just get rid of below two methods from your code if you're already using NSCoding and switch to Codable (Combination of Encodable + Decodable) protocol

required init(coder decoder: NSCoder) // NOT REQUIRED ANY MORE, DECODABLE TAKES CARE OF IT

func encode(with coder: NSCoder) // NOT REQUIRED ANY MORE, ENCODABLE TAKES CARE OF IT

Let's get started with the simplicity of Codable:

Create a custom class or struct you want to store in UserDefaults

struct Person : Codable {
    var name:String
}

OR

class Person : Codable {

    var name:String

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

Save object in UserDefaults as below

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

Load object from UserDefaults as below

guard let savedPersonData = UserDefaults.standard.object(forKey: "kSavedPerson") as? Data else { return }
guard let savedPerson = try? JSONDecoder().decode(Person.self, from: savedPersonData) else { return }

print("\n Saved person name : \(savedPerson.name)")

That's it.

Fraternize answered 6/11, 2019 at 7:33 Comment(1)
I have just spotted that you've posted identical answers for two questions (this one and this one). If you see two questions that can be answered with one answer, then please raise a duplicate flag. If the two questions are different, then please tailor your answer to each question. Thanks!Indogermanic
P
0

I know this question is Old, but I wrote a little library that might be of help:

You store the object this way:

class MyObject: NSObject, Codable {

var name:String!
var lastName:String!

enum CodingKeys: String, CodingKey {
    case name
    case lastName = "last_name"
   }
}

self.myStoredPref = Pref<MyObject>(prefs:UserDefaults.standard,key:"MyObjectKey")
let myObject = MyObject()
//... set the object values
self.myStoredPref.set(myObject)

and then to extract the object back to its original value:

let myStoredValue: MyObject = self.myStoredPref.get()
Perpendicular answered 23/10, 2018 at 5:54 Comment(0)
C
0

You can use PropertyListEncoder to save your custom objects to UserDefaults, after you've implemented the Codable protocol on your class. Let the custom class be User

class User: NSObject, Codable {

  var id: Int?
  var name: String?
  var address: String?
}

Codable implies that it implements both Decodable & Encodable protocols. As name suggests, by implementing Encodable and Decodable, you give your class ability to be encoded and decoded to and from an external representation e.g JSON or Property List.

Now you can save your model translated into property list.

if let encodedData = try? PropertyListEncoder().encode(userModel){
  UserDefaults.standard.set(encodedData, forKey:"YOUR_KEY")
  UserDefaults.standard.synchronize();
}

And you can retrieve your model by using PropertyListDecoder. Because you encoded it as a property list.

if let data = UserDefaults.standard.value(forKey:"YOUR_KEY") as? Data {
     return try? PropertyListDecoder().decode(User.self, from:data)
}
Costa answered 30/4, 2020 at 16:43 Comment(0)
S
-3

You can't store an object in the property list directly; you can only store individual Strings or other primitive types (integers etc.) So you need to store it as individual strings, such as:

   override init() {
   }

   required public init(coder decoder: NSCoder) {
      func decode(obj:AnyObject) -> AnyObject? {
         return decoder.decodeObjectForKey(String(obj))
      }

      self.login = decode(login) as! String
      self.password = decode(password) as! String
      self.firstname = decode(firstname) as! String
      self.surname = decode(surname) as! String
      self.icon = decode(icon) as! UIImage
   }

   public func encodeWithCoder(coder: NSCoder) {
      func encode(obj:AnyObject) {
         coder.encodeObject(obj, forKey:String(obj))
      }

      encode(login)
      encode(password)
      encode(firstname)
      encode(surname)
      encode(icon)
   }
Snowshoe answered 12/4, 2016 at 7:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.