Сохранение пользовательского класса Swift с NSCoding в UserDefaults

в настоящее время я пытаюсь сохранить пользовательский класс Swift в NSUserDefaults. Вот код с моей игровой площадки:

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

может кто-нибудь помочь? Я использую Xcode 6.0.1 на Maverick. Спасибо.

8 ответов


первая проблема заключается в том, что вы должны убедиться, что у вас есть не искаженное имя класса:

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

обратите внимание, что если вы не используете его на игровой площадке, это немного проще, так как вам не нужно регистрировать класс вручную:

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

как @ dan-beaulieu предложил мне ответить на мой собственный вопрос:

вот рабочий код:

Примечание: Demangling имени класса не является необходимым для кода для работы в детских площадок.

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
}

протестировано с 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. Вы можете найти больше о их различия в моем ответе здесь.


в Swift 4 у вас есть новый протокол, который заменяет протокол NSCoding. Это называется Codable и он поддерживает классы и типы Swift! (Перечисление, структуры):

struct CustomStruct: Codable {
    let name: String
    let isActive: Bool
}

В Swift 4 Используйте Codable.

в вашем случае, используйте следующий код.

struct Blog : NSObject, 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) {
}

Swift 3 версия:

class CustomClass: NSObject, NSCoding {

var name = ""
var isActive = false

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

}


для меня я использую мою структуру проще

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)
            NSUserDefaults.standardUserDefaults().synchronize()
        }
    }

}



Use : let userinfo = UserDefaults.UserInformation

вы не можете хранить объект в списке свойств напрямую; вы можете хранить только отдельные строки или другие примитивные типы (целые числа и т. д.) Поэтому вам нужно сохранить его как отдельные строки, такие как:

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