将结构保存到 UserDefaults

Posted

技术标签:

【中文标题】将结构保存到 UserDefaults【英文标题】:Save Struct to UserDefaults 【发布时间】:2017-12-06 04:22:30 【问题描述】:

我有一个要保存到 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))

我想要它,这样每当用户点击该按钮时,它都会,这样每当用户退出应用程序然后再次打开它时,它就会被保存。我该怎么做?

【问题讨论】:

检查***.com/questions/28916535/… 如果你正在尝试 swift 4。有一个新的协议 'Codable' 非常适合这种东西。对于较小的 swift 版本,您必须为您的结构创建字典并手动解析数据 【参考方案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)

【讨论】:

收到无法确认协议可编码的错误 @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) 我发现的一件事是,如果您的结构发生更改(例如,您添加了一个新字段)并且您尝试从 userdefaults 中获取它,您将得到 nil。所以这是一个缺点。 @Micro 这是正确的行为。和这个答案无关!如果类型不再与存储在用户默认值中的类型匹配,则不应该将其从用户默认值中提取出来;旧类型实际上不再存在。这只是你一点一点开发应用程序的一个特点;它与这里的问题或答案无关。 @matt 只是指出这一点,以防有人在他们的应用程序中使用它作为用户对象。如果它发生变化,用户将无法再访问。漏洞?特征?你决定!【参考方案2】:

这是我在主线程中的 UserDefaults 扩展,将获取 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 后台扩展,将获取 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)
    

【讨论】:

我把它当作class func getUser() -&gt; User? UserDefaults.standard.get(object: User.self, for: DefaultKeys.user) user in return user return nil 使用,但是当返回用户值时它给了我一个警告Expression of type 'User?' is unused @EICaptainv2.0 是的,因为是可选的 那么,如何摆脱警告。即使我包装了返回值Expression of type 'User' is unused,警告仍然存在 有没有办法将它与 suiteName 一起使用,如此处所示? ***.com/questions/45607903/… extension UserDefaults static let group = UserDefaults(suiteName: "group.x.x") 试试! UserDefaults.group?.set(object: c, forKey: "ok")【参考方案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) 

如果 titleartist 永远不会发生变异,请考虑将属性声明为常量 (let)。


此答案是在 Swift 4 处于 beta 状态时编写的。同时符合Codable是更好的解决方案。

【讨论】:

我认为将propertyListRepresentation 设置为[String:Any] 可能会更好。 @a_tuo 为什么?这两种类型显然都是String。 Swift 的强类型系统鼓励开发人员尽可能地指定类型。 [String:Any] 如果您有时在 Song 或其他类型中添加 "var count: Int" 可能会更通用。这并不意味着它不安全。 @a_tuo 如果您要添加不同的类型,编译器会告诉您更改字典。考虑当前从未发生过的情况是不良的编程习惯和低效的。 您可以添加任意数量的项目,但我强烈推荐Codable 解决方案。【参考方案4】:

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

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

func addSong(_ song: Song) 
    // This will automatically store new `songs` value 
    // to UserDefaults
    songs.append(song)

【讨论】:

在C#中我们使用default(T),在swift中没有这样的东西,我猜default的目的是使用default关键字作为参数(我们在C#中逐字调用并使用@987654329 @) @HassanTareq,引号`表示default这里不是关键字。 能否对其进行修改/扩展,以便调用者可以使用更标准的 API,例如 UserDefaults.standard.set(_, forKey:) 而不是 @UserDefaultEncoded(key: "songs", default: []) @pkamb,看看property wrappers 是什么,你会发现你不需要修改它。 您的解决方案既 (1) 编码/解码值,又 (2) 将它们保存到标准用户默认值。有没有办法分离关注点,以便属性包装器处理 (1) 但调用者负责 (2) 保存他们想要的位置?例如,您的解决方案在 App Group User Defaults 中不起作用。我想使用自动编码器/解码器,然后使用标准 Swift API 来保存我想要的位置。【参考方案5】:

来自here:

默认对象必须是一个属性列表——即,一个实例(或者对于集合,一个实例的组合): NSData , NSString , NSNumber , NSD日期 , NSArray , 要么 NS词典 .如果要存储任何其他类型的对象,通常应该将其归档以创建 NSData 的实例。

您需要使用NSKeydArchiver。可以在here 和示例here 和here 中找到文档。

【讨论】:

【参考方案6】:

如果您只是想将这组歌曲保存在 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 协议或 swift 4 中的 Codable 协议

编码协议示例:-

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


【讨论】:

【参考方案7】:

我想将用户的设置表示为可观察对象应该很常见。因此,这是一个保持可观察数据与用户默认值同步并针对 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"
    

【讨论】:

【参考方案8】:

斯威夫特 5

如果您希望仅使用 data 格式将 struct 保存为 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)
      
    

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

    可编码(可编码和可解码)。 PropertyListEncoder 和 PropertyListDecoder

首先我们使用Codable(Encodable and Decodable)来保存结构体

保存值示例

  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)

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

【讨论】:

这是一个不必要的复杂解决方案。请参阅上面的答案以获得更简单的方法。【参考方案9】:

这是一个更简单的解决方案

@propertyWrapper
struct CodableUserDefault<Value: Codable> 
    let key: String
    let defaultValue: Value
    private let container: UserDefaults = .standard

    var wrappedValue: Value 
        get 
            guard let data = container.data(forKey: key), let object = try? JSONDecoder().decode(Value.self, from: data) else 
                return defaultValue
            
            
            return object
        
        set 
            container.set(try? JSONEncoder().encode(newValue), forKey: key)
        
    

用法

enum ACodableEnum: String, Codable 
   case first
   case second


class SomeController 

   @CodableUserDefault<ACodableEnum>(key: "key", defaultValue: .first)
    private var aCodableEnum: ACodableEnum


【讨论】:

以上是关于将结构保存到 UserDefaults的主要内容,如果未能解决你的问题,请参考以下文章

将复杂的结构保存到 firebase

将数据结构 c++ 保存到磁盘

将 Numpy 结构数组保存到 *.mat 文件

如何将每次迭代的输出保存到结构中

如何使用 Cloudkit 将字典(或任何其他复杂结构)保存到 iCloud 中?

如何将一组自定义结构保存到 plist swift