将结构保存到UserDefaults


71

我有一个要保存到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,以便每当用户退出应用程序然后将其打开时,它都会被保存。我该怎么做?



如果您正在尝试快速4。有一个新的协议“ Codable”,对这种东西非常有用。对于
较慢的

Answers:


261

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

4
收到无法确认协议编码的错误
Paragon

1
@Paragon:您必须func encode(to encoder: Encoder)在您的结构中实现该方法,然后执行类似操作func 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
我发现的一件事是,如果您的结构发生了变化(例如,您添加了新字段),并且您尝试从userdefaults获取它,那么将得到nil。所以这是一个缺点。

1
@Micro这是正确的行为。与这个答案无关!如果类型不再匹配存储在用户的默认类型,它不应该有可能将其拉出用户默认的; 旧类型实际上不再存在。这只是您逐步开发应用程序的功能;它与这里的问题或答案无关。
马特

1
@matt只是指出这一点,以防有人将其用作其应用程序中的用户对象。如果更改,将无法再访问该用户。虫子?特征?你决定!

20

这是我在主线程中的UserDefaults扩展,用于将get Codable对象设置为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扩展,用于将get Codable对象设置为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)
    }
}

1
我将其用作,class func getUser() -> User? { UserDefaults.standard.get(object: User.self, for: DefaultKeys.user) { user in return user } return nil }但是Expression of type 'User?' is unused在返回用户值时给了我警告
EI Captain v2.0

@ EICaptainv2.0是的,因为是可选的
YannSteph

因此,该怎么做才能消除警告。即使我包装了返回值,警告仍然存在Expression of type 'User' is unused
EI队长v2.0

是否可以将其与suiteName一起使用,如下所示?stackoverflow.com/questions/45607903/…–
柠檬

扩展UserDefaults {static let group = UserDefaults(suiteName:“ group.xx”)}试试!UserDefaults.group?.set(object:c,forKey:“ ok”)
YannSteph

17

如果该结构仅包含符合属性列表的属性,我建议添加一个属性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) }
}

如果titleartist永不突变,考虑将属性声明为常量(let)。


这个答案是在Swift 4处于beta状态时编写的。同时顺应Codable是更好的解决方案。


我认为设置propertyListRepresentation[String:Any]可能会更好。
a_tuo

@a_tuo为什么?两种类型都很明显String。Swift的强类型系统鼓励开发人员尽可能地特定于类型。
vadian

[String:Any]如果有时您在Song或其他类型中添加“ var count:Int”,可能会更通用。这并不意味着它不安全。
a_tuo

1
@a_tuo如果要添加其他类型,则编译器将告诉您更改字典。考虑当前从未发生的情况是不良的编程习惯和低效的。
vadian

1
您可以根据需要添加任意数量的项目,但我强烈建议该Codable解决方案。
vadian

8

这是现代的Swift 5.1 @propertyWrapper,允许以Codable人类可读的JSON字符串的形式存储任何对象:

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

用法:

extension Song: Codable {}

@UserDefaultEncoded(key: "songs", default: [])
var songs: [Song]

在C#中,我们使用default(T)迅速,没有这样的事情,我想目的default是使用default关键字作为参数(我们在C#中称为逐字并使用@default
Hassan Tareq

@HassanTareq,引号`表示default此处不是关键字。
kelin

可以修改/扩展它,以便调用者可以使用更标准的APIUserDefaults.standard.set(_, forKey:)而不是@UserDefaultEncoded(key: "songs", default: [])
pkamb

@pkamb,阅读什么是属性包装器,您将看到不需要修改它。
凯琳20'Mar

您的解决方案都(1)对值进行编码/解码,以及(2)将它们保存为标准用户默认值。有没有一种方法可以分开关注点,以便属性包装器处理(1)但调用方负责(2)保存所需的位置?例如,您的解决方案不适用于“应用组用户默认设置”。我想使用自动编码器/解码器,然后使用标准的Swift API来保存我想要的位置。
pkamb

3

这里:

默认对象必须是属性列表-即NSData,NSString,NSNumber,NSNumber,NSDate,NSArray或NSDictionary的实例(或对于集合而言,是实例的组合)。如果要存储任何其他类型的对象,通常应将其归档以创建NSData实例。

您需要使用NSKeydArchiver。在这里可以找到文档,在这里此处都可以找到示例。


3

如果您只是想将这组歌曲保存在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协议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")
    }

}

1

我认为将用户的设置表示为可观察对象应该很普遍。因此,这是一个使可观察数据与用户默认值保持同步并针对xCode 11.4更新的示例。这也可以在环境对象的上下文中使用。

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

0

迅捷5

如果你想需要保存结构UserDefault只使用数据格式。

枫结构

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

有两种类型可以转换为数据

  1. 可编码(可编码和可解码)。
  2. PropertyListEncoder和PropertyListDecoder

首先,我们使用可编码(可编码和可解码)保存结构

保存值示例

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

取回值

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)

现在我们使用PropertyListEncoderPropertyListDecoder保存结构

保存值示例

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

取回值

  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)

为了方便起见,可以使用any类型将结构保存在userDefault中。


这是不必要的复杂解决方案。有关更简单的方法,请参见上面的答案。
FontFamily
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.