You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题分析与解决方案

核心问题

  1. ContentView无法感知配置变化:ContentView的configStore是普通传入参数,未绑定到SwiftUI的状态系统,当configStore.configuration内部属性变化时,ContentView不会刷新,导致scenePhase变化时读取的是旧值。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 09:59:50