Speichern Sie Struct in UserDefaults

71

Ich habe eine Struktur, die ich in UserDefaults speichern möchte. Hier ist meine Struktur

struct Song {
    var title: String
    var artist: String
}

var songs: [Song] = [
    Song(title: "Title 1", artist "Artist 1"),
    Song(title: "Title 2", artist "Artist 2"),
    Song(title: "Title 3", artist "Artist 3"),
]

In einem anderen ViewController habe ich einen UIButton, der wie folgt an diese Struktur angehängt wird

@IBAction func likeButtonPressed(_ sender: Any) {   
   songs.append(Song(title: songs[thisSong].title, artist: songs[thisSong].artist))
}

Ich möchte, dass die Struktur, wenn der Benutzer auch auf diese Schaltfläche klickt, in UserDefaults gespeichert wird, sodass sie gespeichert wird, wenn der Benutzer die App beendet und sie dann erneut öffnet. Wie würde ich das machen?

Jacob Cavin
quelle
Wenn Sie versuchen, schnell 4. Es gibt ein neues Protokoll 'Codable', das für diese Art von Sachen großartig ist. Für eine weniger schnelle Version müssen Sie ein Wörterbuch für Ihre Struktur erstellen und die Daten manuell analysieren
kathayatnk

Antworten:

261

In Swift 4 ist das ziemlich trivial. Machen Sie Ihre Struktur einfach codierbar, indem Sie sie als das Codable-Protokoll übernehmen:

struct Song:Codable {
    var title: String
    var artist: String
}

Beginnen wir nun mit einigen Daten:

var songs: [Song] = [
    Song(title: "Title 1", artist: "Artist 1"),
    Song(title: "Title 2", artist: "Artist 2"),
    Song(title: "Title 3", artist: "Artist 3"),
]

So bringen Sie das in UserDefaults:

UserDefaults.standard.set(try? PropertyListEncoder().encode(songs), forKey:"songs")

Und so können Sie es später wieder herausholen:

if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
    let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}
matt
quelle
4
Fehler erhalten, der das Protokoll nicht codierbar bestätigt
Paragon
1
@Paragon: Sie müssen die func encode(to encoder: Encoder)Methode in Ihrer Struktur implementieren und dann etwas tun wiefunc encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(title, forKey: .title) try container.encode(artist, forKey: . artist) }
PatrickDotStar
2
Eine Sache, die ich dabei gefunden habe, ist, wenn sich Ihre Struktur ändert (z. B. wenn Sie ein neues Feld hinzufügen) und Sie versuchen, es aus den Benutzerstandards zu erhalten, erhalten Sie null. Das ist also ein Nachteil.
Micro
1
@Micro Und das ist korrektes Verhalten. Es hat nichts mit dieser Antwort zu tun! Wenn der Typ nicht mehr mit dem Typ übereinstimmt, der in den Benutzerstandards gespeichert wurde, sollte es nicht möglich sein, ihn aus den Benutzerstandards zu ziehen. Der alte Typ existiert buchstäblich nicht mehr. Das ist nur eine Funktion, mit der Sie die App Stück für Stück entwickeln. es hat nichts mit der Frage oder Antwort hier zu tun.
Matt
1
@matt Nur darauf hinweisen, falls jemand dies als Benutzerobjekt in seiner App verwendet. Wenn es sich ändert, ist der Benutzer nicht mehr erreichbar. Fehler? Merkmal? DU ENTSCHEIDEST!
Micro
20

Dies ist meine UserDefaults-Erweiterung im Hauptthread , um get Codable- Objekt in UserDefaults festzulegen

// MARK: - UserDefaults extensions

public extension UserDefaults {

    /// Set Codable object into UserDefaults
    ///
    /// - Parameters:
    ///   - object: Codable Object
    ///   - forKey: Key string
    /// - Throws: UserDefaults Error
    public func set<T: Codable>(object: T, forKey: String) throws {

        let jsonData = try JSONEncoder().encode(object)

        set(jsonData, forKey: forKey)
    }

    /// Get Codable object into UserDefaults
    ///
    /// - Parameters:
    ///   - object: Codable Object
    ///   - forKey: Key string
    /// - Throws: UserDefaults Error
    public func get<T: Codable>(objectType: T.Type, forKey: String) throws -> T? {

        guard let result = value(forKey: forKey) as? Data else {
            return nil
        }

        return try JSONDecoder().decode(objectType, from: result)
    }
}

Update Dies ist meine UserDefaults-Erweiterung im Hintergrund , um get Codable- Objekt in UserDefaults zu setzen

// MARK: - JSONDecoder extensions

public extension JSONDecoder {

    /// Decode an object, decoded from a JSON object.
    ///
    /// - Parameter data: JSON object Data
    /// - Returns: Decodable object
    public func decode<T: Decodable>(from data: Data?) -> T? {
        guard let data = data else {
            return nil
        }
        return try? self.decode(T.self, from: data)
    }

    /// Decode an object in background thread, decoded from a JSON object.
    ///
    /// - Parameters:
    ///   - data: JSON object Data
    ///   - onDecode: Decodable object
    public func decodeInBackground<T: Decodable>(from data: Data?, onDecode: @escaping (T?) -> Void) {
        DispatchQueue.global().async {
            let decoded: T? = self.decode(from: data)

            DispatchQueue.main.async {
                onDecode(decoded)
            }
        }
    }
}

// MARK: - JSONEncoder extensions  

public extension JSONEncoder {

    /// Encodable an object
    ///
    /// - Parameter value: Encodable Object
    /// - Returns: Data encode or nil
    public func encode<T: Encodable>(from value: T?) -> Data? {
        guard let value = value else {
            return nil
        }
        return try? self.encode(value)
    }

    /// Encodable an object in background thread
    ///
    /// - Parameters:
    ///   - encodableObject: Encodable Object
    ///   - onEncode: Data encode or nil
    public func encodeInBackground<T: Encodable>(from encodableObject: T?, onEncode: @escaping (Data?) -> Void) {
        DispatchQueue.global().async {
            let encode = self.encode(from: encodableObject)

            DispatchQueue.main.async {
                onEncode(encode)
            }
        }
    }
}       

// MARK: - NSUserDefaults extensions

public extension UserDefaults {

    /// Set Encodable object in UserDefaults
    ///
    /// - Parameters:
    ///   - type: Encodable object type
    ///   - key: UserDefaults key
    /// - Throws: An error if any value throws an error during encoding.
    public func set<T: Encodable>(object type: T, for key: String, onEncode: @escaping (Bool) -> Void) throws {

        JSONEncoder().encodeInBackground(from: type) { [weak self] (data) in
            guard let data = data, let `self` = self else {
                onEncode(false)
                return
            }
            self.set(data, forKey: key)
            onEncode(true)
        }
    }

    /// Get Decodable object in UserDefaults
    ///
    /// - Parameters:
    ///   - objectType: Decodable object type
    ///   - forKey: UserDefaults key
    ///   - onDecode: Codable object
    public func get<T: Decodable>(object type: T.Type, for key: String, onDecode: @escaping (T?) -> Void) {
        let data = value(forKey: key) as? Data
        JSONDecoder().decodeInBackground(from: data, onDecode: onDecode)
    }
}
YannSteph
quelle
1
Ich habe es als verwendet. class func getUser() -> User? { UserDefaults.standard.get(object: User.self, for: DefaultKeys.user) { user in return user } return nil }Aber es gibt mir eine Warnung Expression of type 'User?' is unusedbei der Rückgabe des Benutzerwerts
EI Captain v2.0
@ EICaptainv2.0 Ja, weil optional
YannSteph
Also, was tun, um die Warnung loszuwerden? Warnung bleiben, auch wenn ich den Rückgabewert verpackt habeExpression of type 'User' is unused
EI Captain v2.0
Gibt es eine Möglichkeit, dies mit suiteName wie hier gezeigt zu verwenden? stackoverflow.com/questions/45607903/…
Lemon
Erweiterung UserDefaults {static let group = UserDefaults (suiteName: "group.xx")} try! UserDefaults.group?.set(Objekt: c, forKey: "ok")
YannSteph
17

Wenn die Struktur nur Eigenschaften enthält, die der Eigenschaftsliste entsprechen, empfehle ich, eine Eigenschaft propertyListRepresentationund eine entsprechende initMethode hinzuzufügen

struct Song {

    var title: String
    var artist: String

    init(title : String, artist : String) {
        self.title = title
        self.artist = artist
    }

    init?(dictionary : [String:String]) {
        guard let title = dictionary["title"],
            let artist = dictionary["artist"] else { return nil }
        self.init(title: title, artist: artist)
    }

    var propertyListRepresentation : [String:String] {
        return ["title" : title, "artist" : artist]
    }
}

Speichern einer Reihe von Songs zum UserDefaultsSchreiben

let propertylistSongs = songs.map{ $0.propertyListRepresentation }
UserDefaults.standard.set(propertylistSongs, forKey: "songs")

Array lesen

if let propertylistSongs = UserDefaults.standard.array(forKey: "songs") as? [[String:String]] {
    songs = propertylistSongs.flatMap{ Song(dictionary: $0) }
}

Wenn titleund artistniemals mutiert werden, sollten Sie die Eigenschaften als Konstanten deklarieren ( let).


Diese Antwort wurde geschrieben, während sich Swift 4 im Beta-Status befand. In der Zwischenzeit Codableist die Anpassung die bessere Lösung.

vadian
quelle
Ich denke propertyListRepresentation, es [String:Any]könnte besser sein.
a_tuo
@a_tuo Warum? Beide Typen sind eindeutig String. Das starke Typsystem von Swift ermutigt den Entwickler, so typspezifisch wie möglich zu sein.
Vadian
[String:Any]könnte universeller sein, wenn Sie irgendwann "var count: Int" in den Songoder einen anderen Typ einfügen. Das heißt nicht, dass es nicht sicher ist.
a_tuo
1
@a_tuo Wenn Sie verschiedene Typen hinzufügen möchten, werden Sie vom Compiler aufgefordert, das Wörterbuch zu ändern. Das Betrachten von Fällen, die derzeit nie auftreten, ist eine schlechte Programmiergewohnheit und ineffizient.
Vadian
1
Sie können so viele Elemente hinzufügen, wie Sie möchten, aber ich empfehle die CodableLösung.
Vadian
8

Hier ist ein moderner Swift 5.1 @propertyWrapper, mit dem jedes CodableObjekt in Form einer lesbaren JSON-Zeichenfolge gespeichert werden kann :

@propertyWrapper struct UserDefaultEncoded<T: Codable> {
    let key: String
    let defaultValue: T

    init(key: String, default: T) {
        self.key = key
        defaultValue = `default`
    }

    var wrappedValue: T {
        get {
            guard let jsonString = UserDefaults.standard.string(forKey: key) else {
                return defaultValue
            }
            guard let jsonData = jsonString.data(using: .utf8) else {
                return defaultValue
            }
            guard let value = try? JSONDecoder().decode(T.self, from: jsonData) else {
                return defaultValue
            }
            return value
        }
        set {
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
            guard let jsonData = try? encoder.encode(newValue) else { return }
            let jsonString = String(bytes: jsonData, encoding: .utf8)
            UserDefaults.standard.set(jsonString, forKey: key)
        }
    }
}

Verwendung:

extension Song: Codable {}

@UserDefaultEncoded(key: "songs", default: [])
var songs: [Song]
kelin
quelle
In C #, das wir verwenden default(T), gibt es so etwas in Swift nicht. Ich denke, der Zweck defaultbesteht darin, das defaultSchlüsselwort als Parameter zu verwenden (wir nennen es wörtlich in C # und verwenden es @default)
Hassan Tareq,
@ HassanTareq, Anführungszeichen bedeuten, dass defaulthier kein Schlüsselwort ist.
Kelin
Kann dies geändert / erweitert werden, so dass der Aufrufer eine Standard-API wie UserDefaults.standard.set(_, forKey:)die verwenden kann @UserDefaultEncoded(key: "songs", default: [])?
Pkamb
@pkamb, lesen Sie, was Property Wrapper sind, und Sie werden sehen, dass Sie dies nicht ändern müssen.
Kelin
Ihre Lösung codiert / decodiert beide (1) die Werte UND (2) speichert sie in den Standardbenutzerstandards. Gibt es eine Möglichkeit, die Bedenken so zu trennen, dass der Property Wrapper (1) behandelt , der Anrufer jedoch dafür verantwortlich ist, (2) dort zu speichern, wo er möchte? Beispielsweise funktioniert Ihre Lösung in App Group User Defaults nicht. Ich möchte einen automatischen Encoder / Decodierer verwenden, dann aber die Standard-Swift-API zum Speichern verwenden, wo ich möchte.
Pkamb
3

Von hier aus:

Ein Standardobjekt muss eine Eigenschaftsliste sein, dh eine Instanz von (oder für Sammlungen eine Kombination von Instanzen von): NSData, NSString, NSNumber, NSDate, NSArray oder NSDictionary. Wenn Sie einen anderen Objekttyp speichern möchten, sollten Sie ihn normalerweise archivieren, um eine Instanz von NSData zu erstellen.

Sie müssen verwenden NSKeydArchiver. Dokumentation finden Sie hier hier und Beispiele hier und hier .

Valdmer
quelle
3

Wenn Sie nur versuchen, dieses Array von Songs in UserDefaults zu speichern, und nichts Besonderes, verwenden Sie Folgendes: -

//stores the array to defaults
UserDefaults.standard.setValue(value: songs, forKey: "yourKey")

//retrieving the array

UserDefaults.standard.object(forKey: "yourKey") as! [Song]
//Make sure to typecast this as an array of Song

Wenn Sie ein schweres Array speichern, empfehle ich Ihnen, das NSCoding-Protokoll oder das Codable-Protokoll in Swift 4 zu verwenden

Beispiel für ein Codierungsprotokoll: -

 struct Song {
        var title: String
        var artist: String
    }

    class customClass: NSObject, NSCoding { //conform to nsobject and nscoding

    var songs: [Song] = [
        Song(title: "Title 1", artist "Artist 1"),
        Song(title: "Title 2", artist "Artist 2"),
        Song(title: "Title 3", artist "Artist 3"),
    ]

    override init(arr: [Song])
    self.songs = arr
    }

    required convenience init(coder aDecoder: NSCoder) {
    //decoding your array
    let songs = aDecoder.decodeObject(forKey: "yourKey") as! [Song]

    self.init(are: songs)
    }

    func encode(with aCoder: NSCoder) {
    //encoding
    aCoder.encode(songs, forKey: "yourKey")
    }

}
Dunkle Unschuld
quelle
1

Ich würde mir vorstellen, dass es durchaus üblich sein sollte, die Einstellungen eines Benutzers als beobachtbares Objekt darzustellen. Hier ist ein Beispiel dafür, wie beobachtbare Daten mit den Benutzerstandards synchronisiert und für xCode 11.4 aktualisiert werden. Dies kann auch im Kontext von Umgebungsobjekten verwendet werden.

import SwiftUI

final class UserData: ObservableObject {

    @Published var selectedAddress: String? {
        willSet {
            UserDefaults.standard.set(newValue, forKey: Keys.selectedAddressKey)
        }
    }

    init() {
        selectedAddress = UserDefaults.standard.string(forKey: Keys.selectedAddressKey)
    }

    private struct Keys {
        static let selectedAddressKey = "SelectedAddress"
    }
}
Christopher Hunt
quelle
0

Swift 5

Wenn Sie struct in UserDefault nur mit Daten speichern möchten speichern .

Smaple struct

struct StudentData:Codable{
          
          var id: Int?
          var name: String?
          var createdDate: String?
    
      // for decode the  value
      init(from decoder: Decoder) throws {
        let values = try? decoder.container(keyedBy: codingKeys.self)
        id = try? values?.decodeIfPresent(Int.self, forKey: .id)
        name = try? values?.decodeIfPresent(String.self, forKey: .name)
        createdDate = try? values?.decodeIfPresent(String.self, forKey: .createdDate)
      }
      
      // for encode the  value
      func encode(to encoder: Encoder) throws {
        var values = encoder.container(keyedBy: codingKeys.self)
        try? values.encodeIfPresent(id, forKey: .id)
        try? values.encodeIfPresent(name, forKey: .name)
        try? values.encodeIfPresent(createdDate, forKey: .createdDate)
      }
    }

Es gibt zwei Typen, die als Daten konvertiert werden können

  1. Codierbar (codierbar und decodierbar).
  2. PropertyListEncoder und PropertyListDecoder

Zuerst verwenden wir das Codable (Encodable and Decodable) , um die Struktur zu speichern

Beispiel für Wert speichern

  let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
  guard let data = try? JSONEncoder().encode(value) else {
    fatalError("unable encode as data")
  }
  UserDefaults.standard.set(data, forKey: "Top_student_record")

Wert abrufen

guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
  // write your code as per your requirement
  return
}
guard let value = try? JSONDecoder().decode(StudentData.self, from: data) else {
  fatalError("unable to decode this data")
}
print(value)

Jetzt verwenden wir den PropertyListEncoder und den PropertyListDecoder , um die Struktur zu speichern

Beispiel für Wert speichern

  let value = StudentData(id: 1, name: "Abishek", createdDate: "2020-02-11T11:23:02.3332Z")
  guard let data = try? PropertyListEncoder().encode(value) else {
    fatalError("unable encode as data")
  }
  UserDefaults.standard.set(data, forKey: "Top_student_record")

Wert abrufen

  guard let data = UserDefaults.standard.data(forKey: "Top_student_record") else {
    // write your code as per your requirement
    return
  }
  guard let value = try? PropertyListDecoder().decode(StudentData.self, from: data) else {
    fatalError("unable to decode this data")
  }
  print(value)

Sie können den beliebigen Typ verwenden, um die Struktur in userDefault zu speichern.

Abishek T.
quelle
Dies ist eine unnötig komplexe Lösung. Siehe Antwort oben für einen einfacheren Ansatz.
FontFamily