将结构保存到 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() -> 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)
如果 title
和 artist
永远不会发生变异,请考虑将属性声明为常量 (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)
现在我们使用 PropertyListEncoder 和 PropertyListDecoder 来保存结构
保存值示例
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的主要内容,如果未能解决你的问题,请参考以下文章