使用NSCoding将自定义的Swift类保存到UserDefaults中

87

我目前正在尝试将一个自定义的Swift类保存到NSUserDefaults中。以下是来自我的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")

运行代码时出现以下错误:

执行被中断,原因:信号SIGABRT。

在最后一行 (ud.setObject...)。

同样的代码也会在应用程序中崩溃,并显示以下消息:

"属性列表格式无效:200 (属性列表不能包含类型为'CFType'的对象)"

有人可以帮忙吗?我正在Maverick上使用Xcode 6.0.1。谢谢。


这里有一个很好的教程:https://ios8programminginswift.wordpress.com/2014/08/17/persisting-data-with-nsuserdefaults/ 这里有一个不错的Reddit讨论:http://www.reddit.com/r/swift/comments/28sp3z/whats_the_best_way_to_achieve_persistence_in/ 另外一个好的帖子在这里:http://www.myswiftjourney.me/2014/10/01/simple-persistent-storage-using-nsuserdefaults/ 对于完整的对象存储,我建议使用完整的NSCoding - 下面是两个例子,第二个是我提供的,包含完整的类结构:https://dev59.com/kF8d5IYBdhLWcg3w-mMk#26233274 http://stackoverfl - Steve Rosenberg
12个回答

73

在 Swift 4 或更高版本中,请使用 Codable。

对于您的情况,请使用以下代码。

class Blog: Codable {
   var blogName: String?
}

现在创建它的对象。例如:

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

现在像这样对其进行编码:

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

并像这样解码:

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

如果我想要永久存储它,那么它应该如何工作。谢谢。 - Birju
1
我尝试了这个,但是我收到了错误信息:“类型Blog不符合可解码协议”。 - vofili
@vofili,你从Codable继承了Blog类吗? - Ghulam Rasool
如果我的Blog类中有一个字典[String:Any],我该如何处理它? - Ali Raza

46
第一个问题是你必须确保你有一个非破坏的类名:
@objc(Blog)
class Blog : NSObject, NSCoding {

在将对象存储到用户默认设置中之前,您需要对其进行编码(成NSData):

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

同样地,要恢复对象,您需要对其进行解档:

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

请注意,如果您不在playground中使用它,则会更加简单,因为您不必手动注册类:
if let data = ud.objectForKey("blog") as? NSData {
    let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data)
}

1
谢谢,问题解决了。NSKeyedUnarchiver是缺少的部分。我将使用一个可工作的示例更新我的问题。似乎可以省略unarc.setClass,而是将unarc.decodeObjectForKey向下转换为(在此情况下)Blog类。 - The Lone Coder
1
只有在Playground中工作时,您才需要使用setClass。由于各种原因,类在那里不会自动注册。 - David Berry
上面的 blog 变量不是 if let data = ud.objectForKey("blog") as? NSData { let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data) } 中的自定义对象,在我的情况下,我需要将其转换为我的自定义对象,如下所示:if let data = ud.objectForKey("blog") as? NSData { let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data) if let thisblog = blog as? Blog { return thisblog //这是从 nsuserdefaults 中获取的自定义对象 } } - CaffeineShots

21

正如@dan-beaulieu所建议的,我回答自己的问题:

现在这里是可工作的代码:

注意:对于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
}

如果我的自定义类有UIImage或Data类型,我该怎么办? 我如何在UserDefaults中存储这种类型的类? - Neelam Pursnani

13

这里提供了一个完整的 Swift 4 & 5 解决方案。

首先,在 UserDefaults 扩展中实现辅助方法:

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
    }
}

假设我们想要保存和加载一个具有2个默认字段的自定义对象DummyDummy必须符合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")


这不会永久存储数据。我想在应用程序关闭后存储数据。 - Birju
为什么这个程序不能永久存储数据?@Birju - Awesome-o

9

已使用Swift 2.1和Xcode 7.1.1进行测试。

如果你不需要blogName是可选的(我认为你不需要),我建议稍微改变一下实现方式:

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

测试代码可能如下所示:

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

最后,我想指出使用NSKeyedArchiver并直接将自定义对象数组保存到文件中比使用NSUserDefaults更容易。您可以在我的答案这里了解更多关于它们的区别。

仅供参考:blogName是可选的原因是该对象反映了用户拥有的实际博客。一旦用户添加新博客的URL,博客名称将自动从标题标签中派生。因此,在对象创建时没有博客名称,我想避免称新博客为“未命名”或类似的名称。 - The Lone Coder

7
在Swift 4中,你有一个新的协议来替换NSCoding协议。它被称为Codable,支持类和Swift类型(枚举,结构体):
struct CustomStruct: Codable {
    let name: String
    let isActive: Bool
}

类实现Codable协议,并具有一个可选的字符串数组:'尝试插入非属性列表对象。' - Vyachaslav Gerchicov
这个答案完全是错误的。Codable 不能 替代 NSCoding。直接将对象保存到UserDefaults的唯一方法是使类符合NSCoding协议。由于NSCoding是一个class protocol,它无法应用于结构体/枚举类型。但是,您仍然可以使您的结构体/枚举符合Coding并使用JSONSerializer进行编码以生成Data,然后将其存储在UserDefaults中。 - Josh

4

Swift 3版本:

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

1

我使用自己的结构体,这样更容易。

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

使用方法:

let userinfo = UserDefaults.UserInformation

1
以下是保存自定义对象到UserDefaults的简单示例:
使用“可编码性”(Codable)可以帮助你避免繁琐的手动编码/解码,因此不再需要编写用于保存/检索对象的样板代码。
如果您已经在使用NSCoding,请从您的代码中删除下面两个方法并切换到Codable协议(Encodable + Decodable的组合)。
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

让我们从Codable的简单性开始:
创建一个自定义的类或结构体,您想要存储在UserDefaults中。
struct Person : Codable {
    var name:String
}

OR

class Person : Codable {

    var name:String

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

将对象保存在UserDefaults中,如下所示:
 if let encoded = try? JSONEncoder().encode(Person(name: "Dhaval")) {
     UserDefaults.standard.set(encoded, forKey: "kSavedPerson")
 }

从UserDefaults中加载对象,如下所示:
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)")

就是这样。


我刚刚发现你为两个问题(这一个这一个)发布了完全相同的答案。如果你看到两个可以用一个答案回答的问题,请提出重复标志。如果这两个问题不同,请根据每个问题量身定制你的答案。谢谢! - halfer

0

我知道这个问题很老了,但我写了一个小可能会有所帮助:

您可以通过以下方式存储对象:

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)

然后将对象提取回其原始值:

let myStoredValue: MyObject = self.myStoredPref.get()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接