如何在 SwiftUI 中保存和恢复(持久化)StateObject

问题描述 投票:0回答:3

我很好奇,我们应该如何保存/恢复我们的

ObservableObject
实例中也被标记为
@StateObject
的数据?由于它们是它们包含的任何数据的真实来源,因此自然需要将数据保存到它们或从它们保存数据。

我尝试为我的 Observable 对象添加

Codable
一致性,这看起来很有希望。保存数据工作得很好,但是当我尝试加载数据时,我发现
@StateObject
使包含
ObservableObject
的属性成为只读属性。所以我不能使用
init(from:)
初始化程序加载新实例并用它覆盖属性。 (Xcode 错误:“无法分配给属性:'myStateObject' 是一个只获取属性”)

那我应该怎么解决呢?我能想到的一个解决方案是创建一个中间结构来保存 ObservableObject 中的所有数据,使其符合

Codable
并使用该结构保存和加载数据,然后将值传输到/从
@StateObject
。但这非常混乱,并且是大量代码重复的来源(并且容易出现错误)。

像我现在这样创建一个新实例只是稍微好一点,然后将相关值从新实例复制到状态对象,一次是一个成员。这将回避 get-only 属性问题,并且通过消除对中间结构的需要将减少代码重复,但它仍然不是很好。有哪些更好的方法?

swiftui codable
3个回答
-1
投票

我正在根据Lorem Ipsum其他答案中的评论输入这个答案。

对于少量状态数据,您可以使用 AppStorageSceneStorage 将内容存储在 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()
    }
}

-1
投票

如果您的数据太大而无法存储在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()
            }
        }
    }
}

-1
投票

正如评论中某人所建议的(已删除),将需要持久化的数据分离到一个单独的 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()
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.