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

SwiftUI应用中监听UserDefaults变化的特殊场景解决方法

问题描述

我正在开发一款附带watchOS应用的iOS应用,已配置设置包,当用户在iPhone设置应用中修改设置时,应用的UserDefaults会随之变更。我需要在UserDefaults更新时,通过WatchConnectivity将变更同步至watchOS应用。

为实现同步,我需要监听UserDefaults的变化,但查阅多篇文章后仍未解决问题。@AppStorage属性包装器适用于监听UserDefaults并更新视图,但我的场景不需要更新视图,仅需感知变化并同步至手表。

我编写了如下Swift类:

import Foundation
import Combine

class UserDefaultsHelper: ObservableObject {
@Published var settings1: String = UserDefaults.standard.settings1  {
    didSet {
        UserDefaults.standard.settings1 = settings1
    }
}
@Published var settings2: String = UserDefaults.standard.settings2  {
    didSet {
        UserDefaults.standard.settings2 = settings2
    }
}

private var cancellable: AnyCancellable?
init() {
    cancellable = UserDefaults.standard.publisher(for: \.settings1)
        .sink(receiveValue: { [weak self] newValue in
            guard let self = self else { return }
            if newValue != self.settings1 { // avoid cycling !!
                self.settings1 = newValue
            }
        })
    
    cancellable = UserDefaults.standard.publisher(for: \.settings2)
        .sink(receiveValue: { [weak self] newValue in
            guard let self = self else { return }
            if newValue != self.settings2 { // avoid cycling !!
                self.settings2 = newValue
            }
        })
}
}

// define key for observing
extension UserDefaults {
@objc dynamic var settings1: String {
    get { string(forKey: "settings1") ?? "DEFAULT_VALUE" }
    set { setValue(newValue, forKey: "settings1") }
}

@objc dynamic var settings2: String {
    get { string(forKey: "settings2") ?? "DEFAULT_VALUE" }
    set { setValue(newValue, forKey: "settings2") }
}

但问题在于,我不确定该在何处调用这个类。若在ContentView中调用,会报错:'Static method 'buildBlock' requires that 'UserDefaultsHelper' conform to 'View'。

请问正确的实现方式是什么?我的需求是监听UserDefaults变化,然后调用WatchConnectivity类的方法同步至手表。


解决方案

1. 修正UserDefaultsHelper的订阅存储问题

你当前的代码中,cancellable是单个AnyCancellable变量,第二个订阅会覆盖第一个,导致只有settings2的变化会被监听。需要改成用Set<AnyCancellable>存储所有订阅,同时直接在订阅回调中触发手表同步逻辑:

import Foundation
import Combine
import WatchConnectivity

class UserDefaultsHelper: ObservableObject {
    @Published var settings1: String = UserDefaults.standard.settings1
    @Published var settings2: String = UserDefaults.standard.settings2

    // 用Set存储多个订阅,避免被覆盖
    private var cancellables = Set<AnyCancellable>()

    init() {
        // 监听settings1变化
        UserDefaults.standard.publisher(for: \.settings1)
            .removeDuplicates()
            .sink(receiveValue: { [weak self] newValue in
                self?.settings1 = newValue
                self?.syncSettingsToWatch()
            })
            .store(in: &cancellables)

        // 监听settings2变化
        UserDefaults.standard.publisher(for: \.settings2)
            .removeDuplicates()
            .sink(receiveValue: { [weak self] newValue in
                self?.settings2 = newValue
                self?.syncSettingsToWatch()
            })
            .store(in: &cancellables)
    }

    // 封装WatchConnectivity同步逻辑
    private func syncSettingsToWatch() {
        guard WCSession.isSupported() else { return }
        let session = WCSession.default
        guard session.isReachable else { return }

        let syncData = [
            "settings1": settings1,
            "settings2": settings2
        ]
        do {
            try session.updateApplicationContext(syncData)
        } catch {
            print("同步设置到手表失败: \(error)")
        }
    }
}

// 保持原有的UserDefaults扩展不变
extension UserDefaults {
    @objc dynamic var settings1: String {
        get { string(forKey: "settings1") ?? "DEFAULT_VALUE" }
        set { setValue(newValue, forKey: "settings1") }
    }

    @objc dynamic var settings2: String {
        get { string(forKey: "settings2") ?? "DEFAULT_VALUE" }
        set { setValue(newValue, forKey: "settings2") }
    }
}

2. 正确的实例化位置

UserDefaultsHelper不是View类型,不能直接放到ContentView的视图层级中。推荐在应用全局生命周期中初始化:

方式一:在App入口初始化

在你的@main标记的App结构体中创建实例,确保应用启动时就开始监听:

import SwiftUI

@main
struct YouriOSApp: App {
    // 初始化Helper,应用生命周期内持续存在
    private let settingsHelper = UserDefaultsHelper()

    var body: some Scene {
        WindowGroup {
            ContentView()
            // 如果后续需要在View中访问Helper,可注入到环境
            // .environmentObject(settingsHelper)
        }
    }
}

方式二:做成单例(可选)

如果需要在应用多个模块中访问Helper,可以改成单例模式:

class UserDefaultsHelper: ObservableObject {
    static let shared = UserDefaultsHelper()
    private init() {} // 私有初始化,确保唯一实例

    // 剩余代码和上述一致
}

启动时只需调用_ = UserDefaultsHelper.shared,后续在任意位置通过UserDefaultsHelper.shared访问即可。

3. 核心逻辑说明

  • 初始化后的UserDefaultsHelper会持续监听UserDefaults的变化,无论是App内修改还是系统设置应用修改,都会触发同步逻辑。
  • 完全脱离视图更新依赖,独立完成设置同步,符合你的需求场景。

内容的提问来源于stack exchange,提问作者Mano

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 23:41:32