Guardar clases Swift personalizadas con NSCoding en UserDefaults
nsuserdefaults sigabrt (8)
Actualmente estoy intentando guardar una clase Swift personalizada en NSUserDefaults. Aquí está el código de mi área de juegos:
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")
Cuando ejecuto el código, obtengo el siguiente error
La ejecución fue interrumpida, razón: señal SIGABRT.
en la última línea ( ud.setObject
...)
El mismo código también falla cuando en una aplicación con el mensaje
"La lista de propiedades no es válida para el formato: 200 (las listas de propiedades no pueden contener objetos del tipo ''CFType'')"
¿Alguien puede ayudar? Estoy usando Xcode 6.0.1 en Maverick. Gracias.
Como @ dan-beaulieu sugirió que respondiera mi propia pregunta:
Aquí está el código de trabajo ahora:
Nota: El cambio de nombre de la clase no fue necesario para que el código funcione en 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
}
El primer problema es que debes asegurarte de que tienes un nombre de clase no destruido:
@objc(Blog)
class Blog : NSObject, NSCoding {
Luego, debe codificar el objeto (en un NSData) antes de poder almacenarlo en los valores predeterminados del usuario:
ud.setObject(NSKeyedArchiver.archivedDataWithRootObject(blog), forKey: "blog")
Del mismo modo, para restaurar el objeto necesitarás desarchivarlo:
if let data = ud.objectForKey("blog") as? NSData {
let unarc = NSKeyedUnarchiver(forReadingWithData: data)
unarc.setClass(Blog.self, forClassName: "Blog")
let blog = unarc.decodeObjectForKey("root")
}
Tenga en cuenta que si no lo está utilizando en el patio de recreo es un poco más simple ya que no tiene que registrar la clase a mano:
if let data = ud.objectForKey("blog") as? NSData {
let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data)
}
En Swift 4 tiene un nuevo protocolo que reemplaza el protocolo NSCoding. Se llama Codable
y admite clases y tipos Swift. (Enum, structs):
struct CustomStruct: Codable {
let name: String
let isActive: Bool
}
No puede almacenar un objeto en la lista de propiedades directamente; solo puede almacenar cadenas individuales u otros tipos primitivos (enteros, etc.). Por lo tanto, debe almacenarlo como cadenas individuales, como por ejemplo:
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)
}
Para mí uso mi estructura es más fácil
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
Probado con Swift 2.1 y Xcode 7.1.1
Si no necesitas que blogName sea una opción (que creo que no), te recomendaría una implementación ligeramente diferente:
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)
}
}
el código de prueba podría verse así:
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)")
Finalmente, me gustaría señalar que sería más fácil usar NSKeyedArchiver y guardar su matriz de objetos personalizados en un archivo directamente, en lugar de usar NSUserDefaults. Puedes encontrar más sobre sus diferencias en mi respuesta here .
Swift 3 versión:
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")
}
}
En Swift 4, use codificable.
En tu caso, usa el siguiente código.
struct Blog : NSObject, Codable {
var blogName: String?
}
Ahora crea su objeto. Por ejemplo:
var blog = Blog()
blog.blogName = "My Blog"
Ahora codifícalo así:
if let encoded = try? JSONEncoder().encode(blog) {
UserDefaults.standard.set(encoded, forKey: "blog")
}
y decodifícalo así:
if let blogData = UserDefaults.standard.data(forKey: "blog"),
let blog = try? JSONDecoder().decode(Blog.self, from: blogData) {
}