Сохранить структуру в UserDefaults

У меня есть структура, которую я хочу сохранить в UserDefaults. Здесь моя структура

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

В другом ViewController у меня есть UIButton, который добавляет к этой структуре, как

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

Я хочу, чтобы всякий раз, когда пользователь нажимал на эту кнопку, он сохранял структуру в UserDefaults, чтобы всякий раз, когда пользователь выходил из приложения и затем снова его открывал, он сохранялся. Как бы я это сделал?

Ответ 1

В Swift 4 это довольно тривиально. Сделайте свою структуру кодовым просто, отметив ее как принятие протокола Codable:

struct Song:Codable {
    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"),
]

Здесь, как получить это в UserDefaults:

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

И вот как это сделать позже:

if let data = UserDefaults.standard.value(forKey:"songs") as? Data {
    let songs2 = try? PropertyListDecoder().decode(Array<Song>.self, from: data)
}

Ответ 2

Это мое расширение UserDefaults в главном потоке, чтобы установить получить кодируемой объект в UserDefaults

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

Обновление Это мое расширение UserDefaults в фоновом режиме, чтобы установить получить объект кодируемой в UserDefaults

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

Ответ 3

Если структура содержит только свойства, соответствующие списку свойств, я рекомендую добавить свойство propertyListRepresentation и соответствующий метод init

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

Чтобы сохранить массив песен в UserDefaults напишите

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

Читать массив

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

Если title и artist никогда не будут видоизменены, рассмотрите возможность объявить свойства как константы (let).


Этот ответ был написан, когда Swift 4 был в бета-статусе. Между тем, соответствие Codable является лучшим решением.

Ответ 4

Если вы просто пытаетесь сохранить этот массив песен в UserDefaults, и ничто не может использовать это: -

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

Если вы храните тяжелый массив, я предлагаю вам перейти с протоколом NSCoding или Codable Protocol в swift 4

Пример протокола кодирования: -

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

}

Ответ 5

От здесь:

Объектом по умолчанию должен быть список свойств, то есть экземпляр (или для коллекций, комбинация экземпляров): NSData, NSString, NSNumber, NSDate, NSArray, или NSDictionary, Если вы хотите сохранить какой-либо другой тип объекта, вы должны архивировать его, чтобы создать экземпляр NSData.

Вам нужно использовать NSKeydArchiver. Документацию можно найти здесь и примеры здесь и .