Guardar la clase Swift personalizada con NSCoding en UserDefaults

79

Actualmente estoy tratando de guardar una clase Swift personalizada en NSUserDefaults. Aquí está el código de mi zona 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, aparece el siguiente error

La ejecución fue interrumpida, motivo: señal SIGABRT.

en la última línea ( ud.setObject...)

El mismo código también falla cuando está en una aplicación con el mensaje

"Lista de propiedades no válida para el formato: 200 (las listas de propiedades no pueden contener objetos de tipo 'CFType')"

¿Alguien puede ayudar? Estoy usando Xcode 6.0.1 en Maverick. Gracias.

Georg
fuente
Hay un buen tutorial aquí: ios8programminginswift.wordpress.com/2014/08/17/… Una buena discusión de reddit aquí: reddit.com/r/swift/comments/28sp3z/… Otra buena publicación aquí: myswiftjourney.me/2014/ 10/01 /… Para el almacenamiento completo de objetos, recomiendo la codificación NS completa: dos ejemplos a continuación, el segundo de mí con estructuras de clase completas: stackoverflow.com/questions/26233067/… stackoverfl
Steve Rosenberg

Respuestas:

62

En Swift 4 o superior, use Codable.

En su caso, utilice el siguiente código.

class Blog: Codable {
   var blogName: String?
}

Ahora crea su objeto. Por ejemplo:

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

Ahora codifíquelo así:

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

y decodificarlo así:

if let blogData = UserDefaults.standard.data(forKey: "blog"),
    let blog = try? JSONDecoder().decode(Blog.self, from: blogData) {
}
Ghulam Rasool
fuente
Si quiero almacenarlo permanentemente, entonces cómo debería funcionar. Gracias de antemano
Birju
1
Intenté esto, pero me aparece el mensaje de error, Type Blog no se ajusta al protocolo
Decodable
@vofili, ¿heredaste el Blog de clase de Codable?
Ghulam Rasool
¿Cómo se hace si tengo un diccionario [String: Any] en mi clase de Blog?
Ali Raza
46

El primer problema es que debes asegurarte de tener un nombre de clase no mutilado:

@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, deberá 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)
}
David Berry
fuente
1
Gracias. Eso lo hizo. NSKeyedUnarchiver era la pieza que faltaba. Actualizaré mi pregunta con una muestra funcional. Parece que puede omitir unarc.setClass y, en su lugar, rebajar el unarc.decodeObjectForKey a, en este caso, la clase Blog.
Georg
1
Solo lo necesita setClasssi está trabajando en el patio de juegos, por varias razones, las clases no se registran automáticamente allí.
David Berry
la blogvariable anterior no es el objeto personalizado if let data = ud.objectForKey("blog") as? NSData { let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data) }, en mi caso, necesito convertirlo en mi objeto personalizado de esta maneraif 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 } }
CaffeineShots
21

Como sugirió @ dan-beaulieu, respondo mi propia pregunta:

Aquí está el código de trabajo ahora:

Nota: No era necesario desmandar el nombre de la clase para que el código funcionara 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
}
Georg
fuente
¿Qué pasa si mi clase personalizada tiene el tipo UIImage o Data? ¿Cómo puedo almacenar ese tipo de clase en UserDefaults?
Neelam Pursnani
12

Aquí hay una solución completa para Swift 4 y 5 .

Primero, implemente métodos de ayuda en la UserDefaultsextensión:

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

Digamos, queremos guardar y cargar un objeto personalizado Dummycon 2 campos predeterminados. Dummydebe ajustarse a 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")

Vadim Bulavin
fuente
Esto no almacenará permanentemente los datos. Quiero almacenar datos después de que se elimine la aplicación.
Birju
9

Probado con Swift 2.1 y Xcode 7.1.1

Si no necesita que blogName sea opcional (lo que creo que no es así), 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. Puede encontrar más información sobre sus diferencias en mi respuesta aquí .

Ronny Webers
fuente
Solo para su información: la razón por la que blogName es opcional es que el objeto refleja los blogs reales que tiene un usuario. El nombre del blog se deriva automáticamente de la etiqueta del título una vez que el usuario agrega una URL para un nuevo blog. Entonces, en el momento de la creación del objeto, no hay un nombre de blog y quería evitar llamar a un nuevo blog "sin nombre" o algo similar.
Georg
7

En Swift 4 tienes un nuevo protocolo que reemplaza al protocolo NSCoding. ¡Se llama Codabley admite clases y tipos Swift! (Enum, estructuras):

struct CustomStruct: Codable {
    let name: String
    let isActive: Bool
}
Jeroen Bakker
fuente
La clase implementa Codable y tiene una matriz opcional de cadenas:'Attempt to insert non-property list object
Vyachaslav Gerchicov
Esta respuesta es completamente incorrecta. Codableno no sustituir NSCoding. La única forma de guardar objetos directamente en UserDefaultses hacer que la clase sea NSCodingcompatible. Dado que NSCodinges a class protocol, no se puede aplicar a tipos de estructura / enumeración. Sin embargo, aún puede hacer que su struct / enum sea Codingcompatible y usar JSONSerializerpara codificar Data, que se puede almacenar en UserDefaults.
Josh
4

Versión 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")
    }
}
Nik Yekimov
fuente
1

Yo uso mi propia estructura. Es mucho mas facil.

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

Usar:

let userinfo = UserDefaults.UserInformation
YannSteph
fuente
1

Tendrá que hacer History Model Class para NSCoding así

Swift 4 y 5.

class SSGIFHistoryModel: NSObject, NSCoding {

   var bg_image: String
   var total_like: String
   var total_view: String
   let gifID: String
   var title: String

   init(bg_image: String, total_like: String, total_view: String, gifID: String, title: String) {
    self.bg_image = bg_image
    self.total_like = total_like
    self.total_view = total_view
    self.gifID = gifID
    self.title = title

}

required init?(coder aDecoder: NSCoder) {
    self.bg_image = aDecoder.decodeObject(forKey: "bg_image") as? String ?? ""
    self.total_like = aDecoder.decodeObject(forKey: "total_like") as? String ?? ""
    self.total_view = aDecoder.decodeObject(forKey: "total_view") as? String ?? ""
    self.gifID = aDecoder.decodeObject(forKey: "gifID") as? String ?? ""
    self.title = aDecoder.decodeObject(forKey: "title") as? String ?? ""

}

func encode(with aCoder: NSCoder) {
    aCoder.encode(bg_image, forKey: "bg_image")
    aCoder.encode(total_like, forKey: "total_like")
    aCoder.encode(total_view, forKey: "total_view")
    aCoder.encode(gifID, forKey: "gifID")
    aCoder.encode(title, forKey: "title")
}
}

Después de eso, deberá archivar el objeto en NSData y luego guardarlo en los valores predeterminados del usuario y recuperarlo de los valores predeterminados del usuario y desarchivarlo nuevamente. Puedes archivarlo y desarchivarlo así

func saveGIFHistory() {

    var GIFHistoryArray: [SSGIFHistoryModel] = []

    GIFHistoryArray.append(SSGIFHistoryModel(bg_image: imageData.bg_image, total_like: imageData.total_like, total_view: imageData.total_view, gifID: imageData.quote_id, title: imageData.title))
    if let placesData = UserDefaults.standard.object(forKey: "GIFHistory") as? NSData {
        if let placesArray = NSKeyedUnarchiver.unarchiveObject(with: placesData as Data) as? [SSGIFHistoryModel] {
            for data in placesArray {

                if data.gifID != imageData.quote_id {
                    GIFHistoryArray.append(data)
                }
            }
        }
    }

    let placesData2 = NSKeyedArchiver.archivedData(withRootObject: GIFHistoryArray)
    UserDefaults.standard.set(placesData2, forKey: "GIFHistory")
}

Espero que esto resuelva tu problema

Paresh Mangukiya
fuente
1

Un ejemplo simple de guardar objetos personalizados en UserDefaults sería el siguiente:

Ya no es necesario que escriba el código estándar para guardar / recuperar objetos con la ayuda de THE GREAT 'CODABLE', eso es lo que está ahí para que lo rescates de la codificación / decodificación manual irritante.

Así que simplemente elimine los siguientes dos métodos de su código si ya está utilizando NSCoding y cambie al protocolo Codificable (Combinación de codificación + descodificación)

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

Comencemos con la simplicidad de Codable:

Cree una clase o estructura personalizada que desee almacenar en UserDefaults

struct Person : Codable {
    var name:String
}

OR

class Person : Codable {

    var name:String

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

Guarde el objeto en UserDefaults como se muestra a continuación

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

Cargue el objeto de UserDefaults como se muestra a continuación

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

Eso es.

Dhaval H. Nena
fuente
Acabo de descubrir que ha publicado respuestas idénticas para dos preguntas ( esta y esta ). Si ve dos preguntas que pueden responderse con una sola respuesta, levante una bandera duplicada. Si las dos preguntas son diferentes, adapte su respuesta a cada pregunta. ¡Gracias!
halfer
0

Sé que esta pregunta es antigua, pero escribí una pequeña biblioteca que podría ser de ayuda:

Almacena el objeto de esta manera:

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)

y luego para extraer el objeto a su valor original:

let myStoredValue: MyObject = self.myStoredPref.get()
Lena Bru
fuente
0

Puede usar PropertyListEncoder para guardar sus objetos personalizados en UserDefaults, después de haber implementado el protocolo Codable en su clase. Deje que la clase personalizada sea Usuario

class User: NSObject, Codable {

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

Codificable implica que implementa protocolos decodificables y codificables. Como sugiere el nombre, al implementar Encodable y Decodable, le da a su clase la capacidad de ser codificada y decodificada hacia y desde una representación externa, por ejemplo, JSON o Property List.

Ahora puede guardar su modelo traducido a la lista de propiedades.

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

Y puede recuperar su modelo utilizando PropertyListDecoder. Porque lo codificó como una lista de propiedades.

if let data = UserDefaults.standard.value(forKey:"YOUR_KEY") as? Data {
     return try? PropertyListDecoder().decode(User.self, from:data)
}
Umair
fuente
-3

No puede almacenar un objeto en la lista de propiedades directamente; solo puede almacenar cadenas individuales u otros tipos primitivos (enteros, etc.), por lo que debe almacenarlas como cadenas individuales, como:

   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)
   }
Mikhail Baynov
fuente