iOS开发:Observable对象关联Environment后数据保存异常排查
问题:iOS @Observable配置类与文件读写关联失效,保存旧值
我已掌握iOS应用读写文件数据的方法,也了解如何将数据放入@Environment供各视图使用,但无法将二者关联起来。定义了可编码的@Observable配置类和对应的ConfigStore后,在UI中修改配置值,点击Home按钮触发保存时,保存的却是旧值,日志显示修改后的true未同步到保存流程中。
代码实现
1. 可编码的Config类
@Observable class Config: Codable { var someConfigVal: Bool = false }
2. ConfigStore与Environment扩展
@Observable class ConfigStore { var configuration: Config = Config() private static func fileURL() throws -> URL { try FileManager.default.url( for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false ) .appendingPathComponent("config.data") } func load() async throws { let task = Task<Config, Error> { let fileURL = try Self.fileURL() guard let data = try? Data(contentsOf: fileURL) else { return Config() } let storedConfig = try JSONDecoder().decode(Config.self, from: data) print("loaded", storedConfig.someConfigVal) return storedConfig } let appConfig = try await task.value self.configuration = appConfig print("load", self.configuration.someConfigVal) } func save(appConfig: Config) async throws { print("Some val on save", appConfig.someConfigVal) let task = Task { let data = try JSONEncoder().encode(appConfig) let outfile = try Self.fileURL() try data.write(to: outfile) } _ = try await task.value } } extension EnvironmentValues { var configurationStore: ConfigStore { get { self[ConfigStoreKey.self] } set { self[ConfigStoreKey.self] = newValue } } } private struct ConfigStoreKey: EnvironmentKey { static let defaultValue: ConfigStore = ConfigStore() }
3. 主应用入口
@main struct StoreTestApp: App { @State private var configStore = ConfigStore() var body: some Scene { WindowGroup { ContentView(configStore: configStore) { Task { print("In save task", configStore.configuration.someConfigVal) do { try await configStore.save(appConfig: configStore.configuration) } catch { fatalError(error.localizedDescription) } } } .task { do { try await configStore.load() } catch { fatalError(error.localizedDescription) } } // .environment(configStore) } } }
4. UI视图代码
struct ContentView: View { @Environment(\.scenePhase) private var scenePhase // @Environment(\.configurationStore) private var configStore var configStore: ConfigStore let saveAction: () -> Void var body: some View { TabView { ShowView() .tabItem { Label("Show", systemImage: "magnifyingglass") } ParentEditView() .tabItem { Label("Edit", systemImage: "pencil") } } .onChange(of: scenePhase) { _, newPhase in if newPhase == .inactive { print("Scene is inactive", configStore.configuration.someConfigVal) saveAction() } } .environment(configStore) } } struct ShowView: View { @Environment(\.configurationStore) private var configStore var body: some View { VStack { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) Text(String(configStore.configuration.someConfigVal)) } .padding() } } struct ParentEditView: View { @Environment(\.configurationStore) private var configStore var body: some View { EditView(configStore: configStore) } } struct EditView: View { @Bindable var configStore: ConfigStore var body: some View { Toggle("Some val", isOn: $configStore.configuration.someConfigVal) .onChange(of: configStore.configuration.someConfigVal, { print(configStore.configuration.someConfigVal) }) } }
运行日志
loaded false load false true Scene is inactive false In save task false Some val on save false
问题分析与解决方案
核心问题
- ContentView无法感知配置变化:ContentView的
configStore是普通传入参数,未绑定到SwiftUI的状态系统,当configStore.configuration内部属性变化时,ContentView不会刷新,导致scenePhase变化时读取的是旧值。 - Save方法依赖外部传入参数:原
save方法需要传入appConfig,容易因闭包捕获时机问题传入旧实例。
修改步骤
1. 优化ConfigStore的Save方法
移除外部参数,直接保存当前实例的配置,避免传入旧值:
func save() async throws { print("Some val on save", self.configuration.someConfigVal) let task = Task { let data = try JSONEncoder().encode(self.configuration) let outfile = try Self.fileURL() try data.write(to: outfile) } _ = try await task.value }
2. 调整StoreTestApp的环境传递
将ConfigStore通过环境传递,而非直接作为参数传入ContentView:
@main struct StoreTestApp: App { @State private var configStore = ConfigStore() var body: some Scene { WindowGroup { ContentView() .environment(\.configurationStore, configStore) .task { do { try await configStore.load() } catch { fatalError(error.localizedDescription) } } } } }
3. 修改ContentView获取配置方式
通过@Environment获取ConfigStore,确保能感知内部变化,并直接在内部实现保存逻辑:
struct ContentView: View { @Environment(\.scenePhase) private var scenePhase @Environment(\.configurationStore) private var configStore var body: some View { TabView { ShowView() .tabItem { Label("Show", systemImage: "magnifyingglass") } ParentEditView() .tabItem { Label("Edit", systemImage: "pencil") } } .onChange(of: scenePhase) { _, newPhase in if newPhase == .inactive { print("Scene is inactive", configStore.configuration.someConfigVal) Task { do { try await configStore.save() } catch { fatalError(error.localizedDescription) } } } } } }
4. 可选优化Load方法的错误处理
避免隐藏读取错误,明确处理文件不存在或读取失败的情况:
func load() async throws { let task = Task<Config, Error> { let fileURL = try Self.fileURL() do { let data = try Data(contentsOf: fileURL) let storedConfig = try JSONDecoder().decode(Config.self, from: data) print("loaded", storedConfig.someConfigVal) return storedConfig } catch { print("Load error: \(error.localizedDescription), using default config") return Config() } } let appConfig = try await task.value self.configuration = appConfig print("load", self.configuration.someConfigVal) }
修改后预期日志
loaded false load false true Scene is inactive true Some val on save true
内容的提问来源于stack exchange,提问作者Gene
相关产品推荐
相关产品推荐

