Zapisywanie niestandardowej klasy Swift z NSCoding do UserDefaults

79

Obecnie próbuję zapisać niestandardową klasę Swift w NSUserDefaults. Oto kod z mojego 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")

Po uruchomieniu kodu pojawia się następujący błąd

Wykonywanie zostało przerwane, przyczyna: sygnał SIGABRT.

w ostatniej linii ( ud.setObject...)

Ten sam kod ulega również awarii w aplikacji z komunikatem

„Lista właściwości jest nieprawidłowa dla formatu: 200 (listy właściwości nie mogą zawierać obiektów typu„ CFType ”)”

Czy ktoś może pomóc? Używam Xcode 6.0.1 na Maverick. Dzięki.

Georg
źródło
Tutaj jest dobry poradnik: ios8programminginswift.wordpress.com/2014/08/17/ ... Dobra dyskusja na temat reddita : reddit.com/r/swift/comments/28sp3z/ ... Kolejny dobry post: myswiftjourney.me/2014/ 10/01 /… Do pełnego przechowywania obiektów polecam pełne NSCoding - dwa przykłady poniżej, drugi ode mnie z pełnymi strukturami klas: stackoverflow.com/questions/26233067/… stackoverfl
Steve Rosenberg

Odpowiedzi:

62

W wersji Swift 4 lub nowszej użyj Codable.

W twoim przypadku użyj następującego kodu.

class Blog: Codable {
   var blogName: String?
}

Teraz stwórz jego obiekt. Na przykład:

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

Teraz zakoduj to w ten sposób:

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

i dekoduj w ten sposób:

if let blogData = UserDefaults.standard.data(forKey: "blog"),
    let blog = try? JSONDecoder().decode(Blog.self, from: blogData) {
}
Ghulam Rasool
źródło
Jeśli chcę go przechowywać na stałe, to jak powinno działać. Z góry dziękuję
Birju
1
Próbowałem tego, ale otrzymuję komunikat o błędzie, Typ Blog nie jest zgodny z protokołem Decodable
vofili
@vofili, czy odziedziczyłeś blog klasy po Codable?
Ghulam Rasool,
jak to zrobić, jeśli mam słownik [String: Any] w mojej klasie Blog?
Ali Raza,
46

Pierwszy problem polega na tym, że musisz upewnić się, że masz niezmienioną nazwę klasy:

@objc(Blog)
class Blog : NSObject, NSCoding {

Następnie musisz zakodować obiekt (do NSData), zanim będziesz mógł zapisać go w domyślnych ustawieniach użytkownika:

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

Podobnie, aby przywrócić obiekt, musisz go przywrócić z archiwum:

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

Pamiętaj, że jeśli nie używasz go na placu zabaw, jest to trochę prostsze, ponieważ nie musisz ręcznie rejestrować zajęć:

if let data = ud.objectForKey("blog") as? NSData {
    let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data)
}
David Berry
źródło
1
Dzięki. Zrobiło to. Brakującym elementem był NSKeyedUnarchiver. Zaktualizuję moje pytanie próbką roboczą. Wygląda na to, że możesz pominąć unarc.setClass i zamiast tego obniżyć unarc.decodeObjectForKey do - w tym przypadku - klasy Blog.
Georg,
1
Potrzebujesz go tylko setClasswtedy, gdy pracujesz na Placu Zabaw, z różnych powodów zajęcia nie są tam automatycznie rejestrowane.
David Berry,
blogzmienna powyżej nie jest przedmiotem zwyczaju if let data = ud.objectForKey("blog") as? NSData { let blog = NSKeyedUnarchiver.unarchiveObjectWithData(data) }, w moim przypadku muszę oddać go do mojego niestandardowego obiektu takif 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

Jak zasugerował @ dan-beaulieu, odpowiadam na własne pytanie:

Oto działający kod:

Uwaga: wyodrębnienie nazwy klasy nie było konieczne, aby kod działał w 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
źródło
Co się stanie, jeśli moja klasa niestandardowa ma typ UIImage lub Data? Jak mogę przechowywać tego rodzaju klasy w UserDefaults?
Neelam Pursnani
12

Oto kompletne rozwiązanie dla Swift 4 i 5 .

Najpierw zaimplementuj metody pomocnicze w UserDefaultsrozszerzeniu:

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

Powiedzmy, że chcemy zapisać i załadować niestandardowy obiekt Dummyz 2 domyślnymi polami. Dummymuszą być zgodne z 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
źródło
Nie spowoduje to trwałego przechowywania danych. Chcę przechowywać dane po zabiciu aplikacji.
Birju
9

Przetestowano za pomocą Swift 2.1 i Xcode 7.1.1

Jeśli nie potrzebujesz, aby blogName był opcjonalny (a myślę, że nie), polecam nieco inną implementację:

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

kod testowy mógłby wyglądać następująco:

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

Na koniec chciałbym zwrócić uwagę, że byłoby łatwiej używać NSKeyedArchiver i zapisywać tablicę niestandardowych obiektów bezpośrednio do pliku, zamiast używać NSUserDefaults. Więcej na temat ich różnic można znaleźć w mojej odpowiedzi tutaj .

Ronny Webers
źródło
Do Twojej wiadomości: powodem, dla którego nazwa blogu jest opcjonalna, jest fakt, że obiekt odzwierciedla rzeczywiste blogi użytkownika. Nazwa bloga jest automatycznie tworzona na podstawie tagu tytułu, gdy użytkownik doda adres URL nowego bloga. Tak więc w czasie tworzenia obiektu nie ma nazwy bloga i chciałem uniknąć nazywania nowego bloga „bez nazwy” lub czegoś podobnego.
Georg
7

W Swift 4 masz nowy protokół, który zastępuje protokół NSCoding. Nazywa się Codablei obsługuje klasy i typy Swift! (Enum, structs):

struct CustomStruct: Codable {
    let name: String
    let isActive: Bool
}
Jeroen Bakker
źródło
class implementuje Codable i ma opcjonalną tablicę ciągów:'Attempt to insert non-property list object
Vyachaslav Gerchicov
Ta odpowiedź jest całkowicie błędna. Codableczy nie zastąpić NSCoding. Jedynym sposobem bezpośredniego zapisywania obiektów UserDefaultsjest zapewnienie NSCodingzgodności klasy . Ponieważ NSCodingjest a class protocol, nie można go zastosować do typów struktur / wyliczeń. Jednak nadal możesz zapewnić Codingzgodność struktury / wyliczenia i używać go JSONSerializerdo kodowania Data, które można przechowywać w plikach UserDefaults.
Josh
4

Wersja 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
źródło
1

Używam własnej struktury. To dużo łatwiejsze.

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

Używać:

let userinfo = UserDefaults.UserInformation
YannSteph
źródło
1

Będziesz musiał utworzyć klasę modelu historii na NSCoding w ten sposób

Swift 4 i 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")
}
}

Następnie będziesz musiał zarchiwizować obiekt w NSData, a następnie zapisać go w ustawieniach domyślnych użytkownika i odzyskać z ustawień domyślnych użytkownika, a następnie ponownie zarchiwizować. Możesz archiwizować i cofać archiwizację w ten sposób

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

Mam nadzieję, że to rozwiąże twój problem

Paresh Mangukiya
źródło
1

Prosty przykład zapisywania obiektów niestandardowych w UserDefaults wyglądałby poniżej:

Nie musisz już pisać standardowego kodu do zapisywania / pobierania obiektów za pomocą WIELKIEGO „KODU”, to jest to, co możesz uratować od irytującego ręcznego kodowania / dekodowania.

Po prostu pozbądź się poniższych dwóch metod z kodu, jeśli już używasz NSCoding i przełącz się na protokół Codable (Combination of 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

Zacznijmy od prostoty Codable:

Utwórz niestandardową klasę lub strukturę, którą chcesz przechowywać w UserDefaults

struct Person : Codable {
    var name:String
}

OR

class Person : Codable {

    var name:String

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

Zapisz obiekt w UserDefaults, jak poniżej

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

Załaduj obiekt z UserDefaults, jak poniżej

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

Otóż ​​to.

Dhaval H. Nena
źródło
Właśnie zauważyłem, że opublikowałeś identyczne odpowiedzi na dwa pytania ( to jedno i to jedno ). Jeśli widzisz dwa pytania, na które można odpowiedzieć jedną odpowiedzią, podnieś podwójną flagę. Jeśli te dwa pytania są różne, dostosuj swoją odpowiedź do każdego pytania. Dzięki!
halfer
0

Wiem, że to pytanie jest stare, ale napisałem małą bibliotekę, która może być pomocna:

Przechowujesz obiekt w ten sposób:

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)

a następnie wyodrębnić obiekt z powrotem do jego pierwotnej wartości:

let myStoredValue: MyObject = self.myStoredPref.get()
Lena Bru
źródło
0

Możesz użyć PropertyListEncoder, aby zapisać obiekty niestandardowe w UserDefaults po zaimplementowaniu protokołu Codable w swojej klasie. Niech klasą niestandardową będzie User

class User: NSObject, Codable {

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

Codable oznacza, że ​​implementuje oba protokoły Decodable i Encodable. Jak sama nazwa wskazuje, implementując Encodable and Decodable, dajesz swojej klasie możliwość kodowania i dekodowania do iz zewnętrznej reprezentacji, np. JSON lub Property List.

Teraz możesz zapisać swój model przetłumaczony na listę właściwości.

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

Możesz pobrać model za pomocą PropertyListDecoder. Ponieważ zakodowałeś to jako listę właściwości.

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

Nie możesz bezpośrednio przechowywać obiektu na liście właściwości; możesz przechowywać tylko pojedyncze ciągi lub inne typy pierwotne (liczby całkowite itp.) Więc musisz przechowywać je jako pojedyncze ciągi, takie jak:

   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)
   }
Michaił Baynov
źródło