【问题标题】:Save Struct to UserDefaults将结构保存到 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))
}

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

【问题讨论】:

  • 如果你正在尝试 swift 4。有一个新的协议 'Codable' 非常适合这种东西。对于较小的 swift 版本,您必须为您的结构创建字典并手动解析数据

标签: ios swift struct nsuserdefaults


【解决方案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 一起使用,如此处所示? stackoverflow.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 和示例herehere 中找到文档。

【讨论】:

    【解决方案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)
              }
            }
        

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

        1. 可编码(可编码和可解码)。
        2. 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
        
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-01-12
          • 2021-02-07
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多