我很好奇,我们应该如何保存/恢复我们的
ObservableObject
实例中也被标记为@StateObject
的数据?由于它们是它们包含的任何数据的真实来源,因此自然需要将数据保存到它们或从它们保存数据。
我尝试为我的 Observable 对象添加
Codable
一致性,这看起来很有希望。保存数据工作得很好,但是当我尝试加载数据时,我发现 @StateObject
使包含 ObservableObject
的属性成为只读属性。所以我不能使用 init(from:)
初始化程序加载新实例并用它覆盖属性。 (Xcode 错误:“无法分配给属性:'myStateObject' 是一个只获取属性”)
那我应该怎么解决呢?我能想到的一个解决方案是创建一个中间结构来保存 ObservableObject 中的所有数据,使其符合
Codable
并使用该结构保存和加载数据,然后将值传输到/从@StateObject
。但这非常混乱,并且是大量代码重复的来源(并且容易出现错误)。
像我现在这样创建一个新实例只是稍微好一点,然后将相关值从新实例复制到状态对象,一次是一个成员。这将回避 get-only 属性问题,并且通过消除对中间结构的需要将减少代码重复,但它仍然不是很好。有哪些更好的方法?
我正在根据Lorem Ipsum的其他答案中的评论输入这个答案。
对于少量状态数据,您可以使用 AppStorage 或 SceneStorage 将内容存储在 UserDefaults 中。这是为了在每个设备的持久存储中存储非常少量的某些类型的数据(NSData、NSString、NSNumber、NSDate、NSArray 或 NSDictionary)。
这里有几个文件展示了如何做到这一点。通过向 ObservableObject 添加 Codable 和 RawRepresentable 一致性,您可以启用使用
@AppStorage
装饰器的选项。它将负责在正确的时间存储和检索数据,包括初始化。
StatePackage.swift:
import SwiftUI
class StatePackage: ObservableObject, Codable, RawRepresentable {
@Published var label = "Text"
@Published var icon = "questionmark.app"
// Explicit default initializer needed because other inits prevent the
// automatic compiler-generated initializer
init() {}
// The next three items are to conform to Codable
enum CodingKeys: CodingKey {
case label, icon
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
label = try container.decode(String.self, forKey: .label)
icon = try container.decode(String.self, forKey: .icon)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(label, forKey: .label)
try container.encode(icon, forKey: .icon)
}
// The next two items are to conform to RawRepresentable
required init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode(StatePackage.self, from: data)
else {
return nil
}
label = result.label
icon = result.icon
}
var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}
ContentView.swift:
import SwiftUI
struct ContentView: View {
@AppStorage("StatePackage") var statePackage = StatePackage()
var body: some View {
Form {
TextField("Label", text: $statePackage.label)
TextField("Icon", text: $statePackage.icon)
Section("Preview") {
Label(statePackage.label, systemImage: statePackage.icon)
}
}
.padding()
}
}
如果您的数据太大而无法存储在UserDefaults中,您可以尝试使用@ObservedObject 而不是@StateObject,这样初始化和恢复对象的责任在层次结构中更高,而不使用@StateObject 将提供您可以访问更多 Swift 功能,这些功能将允许您保存和恢复数据。
例如,下面的三个文件显示了如何通过让 App 对象负责保存和恢复保存状态数据的对象来达到预期的结果。保持该状态的变量的声明(即
statePackage
)没有任何装饰器声明它是真理的来源,但它仍然是真理的来源。从语义上讲,那里可能有问题,但这似乎没问题。下游视图正确使用 @ObservedObject
来表达对对象更改的兴趣,正如您已经习惯的那样。
真的,如果 Apple 给我们一个
@PersistentStateObject("StatePackage.json")
装饰器,它会做同样的事情,那就太好了。
StatePackage.swift:
import SwiftUI
class StatePackage: ObservableObject, Codable {
@Published var label = "Text"
@Published var icon = "questionmark.app"
// Explicit default initializer needed because init(from:) prevents the
// automatic compiler-generated initializer
init() {}
// The next three items are to conform to Codable
enum CodingKeys: CodingKey {
case label, icon
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
label = try container.decode(String.self, forKey: .label)
icon = try container.decode(String.self, forKey: .icon)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(label, forKey: .label)
try container.encode(icon, forKey: .icon)
}
}
ContentView.swift:
import SwiftUI
struct ContentView: View {
@ObservedObject var statePackage: StatePackage
var body: some View {
Form {
TextField("Label", text: $statePackage.label)
TextField("Icon", text: $statePackage.icon)
Section("Preview") {
Label(statePackage.label, systemImage: statePackage.icon)
}
}
}
}
PersistStateObjectApp.swift:
import SwiftUI
@main
struct PersistStateObjectApp: App {
private var statePackage: StatePackage
@Environment(\.scenePhase) private var scenePhase
// we load the state data from persistent storage at initialization time
init() {
do {
let fileURL = Self.persistentFileURL("StatePackage.json")
let data = try Data(contentsOf: fileURL)
statePackage = try JSONDecoder().decode(StatePackage.self, from: data)
} catch {
// If we can't load from persistent store for any reason, fall back on default settings
statePackage = StatePackage()
}
}
// The state data is saved to a file in our Documents folder
static func persistentFileURL(_ filename: String) -> URL {
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return url.appendingPathComponent(filename)
}
func savePersistentData() {
do {
let fileURL = Self.persistentFileURL("StatePackage.json")
let data = try JSONEncoder().encode(statePackage)
try data.write(to: fileURL)
} catch {
fatalError("Unable to save state")
// handle this better in real code
}
}
var body: some Scene {
WindowGroup {
ContentView(statePackage: statePackage)
}
.onChange(of: scenePhase) { phase in
if phase == .background {
savePersistentData()
}
}
}
}
正如评论中某人所建议的(已删除),将需要持久化的数据分离到一个单独的 Codable 结构中将使我们能够使用 Codable 机制来保存/加载该数据,而不会与 SwiftUI 的管道发生冲突。如果您的数据很容易以这种方式重新排列(我的不是)并且您不介意在访问期间进行额外的间接级别(我不会),这可能是一个很好的解决方案。
这里有一些代码显示了这个的实现。
ContentView.swift(注意所有额外的“.data”间接寻址):
import SwiftUI
struct ContentView: View {
@StateObject var statePackage: StatePackage = StatePackage()
@Environment(\.scenePhase) private var scenePhase
var body: some View {
Form {
TextField("Label", text: $statePackage.data.label)
TextField("Icon", text: $statePackage.data.icon)
Section("Preview") {
Label(statePackage.data.label, systemImage: statePackage.data.icon)
}
}
.onChange(of: scenePhase) { phase in
if phase == .background {
statePackage.saveToStore()
} else if phase == .active {
statePackage.loadFromStore()
}
}
}
}
和StatePackage.swift:
import SwiftUI
class StatePackage: ObservableObject {
struct StateStruct: Codable {
var label = "name"
var icon = "questionmark"
}
// All of our persisting data is pushed down one level into an encapsulating
// struct so that we can use standard Codable methods to load/save it easily
@Published var data = StateStruct()
// Defining that we save our data in the Documents folder
static func persistentFileURL() -> URL {
let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
return url.appendingPathComponent("StatePackage.json")
}
func saveToStore() {
print("save to Store")
do {
let url = Self.persistentFileURL()
try JSONEncoder().encode(data).write(to: url)
} catch {
fatalError("Unable to save state")
// handle this better in real code
}
}
func loadFromStore() {
print("load from store")
do {
data = try JSONDecoder().decode(Self.StateStruct.self,
from: Data(contentsOf: Self.persistentFileURL()))
} catch {
// If we can't load from persistent store for any reason, fall back on default settings
data = StateStruct()
}
}
}