SwiftUI 的 @Observable
宏能让数据变更自动触发 UI 更新,但在某些场景下会导致严重的无限循环问题:
@Observable
class DataManager {
var data: [String: Int] = [:]
func calculateValue(for key: String) -> Int {
let result = performCalculation()
data[key] = result // 这里修改了被观察的属性
return result
}
}
当视图依赖 calculateValue
方法时,每次视图刷新都会执行该方法,导致 data
字典被修改,这又会触发新一轮视图刷新,形成无限循环。
问题表现 #
- 应用启动极慢或无法启动
- 控制台日志显示同一方法被反复调用
- CPU 使用率持续高位
- 应用内存占用急剧增加
解决方案 #
1. 使用 @ObservationIgnored 隔离变更通知 #
最简单的解决方案是标记那些频繁变化但不需要触发 UI 更新的属性。
@Observable
class DataManager {
@ObservationIgnored var cache: [String: Int] = [:] // 变更不会触发 UI 更新
func getValue(for key: String) -> Int {
if let cached = cache[key] {
return cached
}
let result = performCalculation()
cache[key] = result // 安全地更新缓存
return result
}
}
2. 使用版本控制触发有意义的更新 #
引入版本标识,只在需要时才触发 UI 更新:
@Observable
class DataManager {
@ObservationIgnored private var cache: [String: Int] = [:]
private var updateVersion = 0
var cacheVersion: Int { updateVersion } // 视图可以依赖这个属性
func getCachedData() -> [String: Int] {
return cache // 读取缓存数据
}
func refreshData() {
// 计算新值并更新缓存
cache["key1"] = calculateValue1()
cache["key2"] = calculateValue2()
// 只在所有计算完成后,更新一次版本触发 UI 刷新
updateVersion += 1
}
}
在视图中使用:
struct DataView: View {
@Environment(DataManager.self) private var dataManager
var body: some View {
List {
ForEach(Array(dataManager.getCachedData().keys), id: \.self) { key in
if let value = dataManager.getCachedData()[key] {
Text("\(key): \(value)")
}
}
}
.id(dataManager.cacheVersion) // 确保视图在版本变化时刷新
.onAppear {
dataManager.refreshData()
}
}
}
3. 分离数据模型与视图模型 #
将数据处理与 UI 状态管理分离:
// 普通类,不使用观察者模式
class DataCalculator {
var cache: [String: Int] = [:]
func calculateValues() -> [String: Int] {
// 执行复杂计算
cache["value1"] = complexCalculation1()
cache["value2"] = complexCalculation2()
return cache
}
}
// 与 UI 交互的视图模型
@Observable
class DataViewModel {
private let calculator = DataCalculator()
private(set) var displayData: [String: Int] = [:]
func refreshData() {
// 在这里控制何时更新 UI 相关状态
displayData = calculator.calculateValues()
}
}
4. 实现后台定时更新机制 #
对于需要定期刷新的数据,使用后台任务避免阻塞 UI:
@Observable
class DataManager {
@ObservationIgnored private var cache: [String: Int] = [:]
private var cacheVersion = 0
private var refreshTimer: Timer?
var latestData: [String: Int] {
_ = cacheVersion // 创建依赖
return cache
}
init() {
startPeriodicRefresh()
}
deinit {
stopPeriodicRefresh()
}
private func startPeriodicRefresh() {
refreshTimer = Timer.scheduledTimer(
timeInterval: 60, // 每分钟刷新一次
target: self,
selector: #selector(refreshCache),
userInfo: nil,
repeats: true
)
}
private func stopPeriodicRefresh() {
refreshTimer?.invalidate()
refreshTimer = nil
}
@objc private func refreshCache() {
DispatchQueue.global(qos: .background).async { [weak self] in
// 在后台线程执行计算
let newData = self?.performHeavyCalculations() ?? [:]
// 在主线程更新状态
DispatchQueue.main.async {
guard let self = self else { return }
self.cache = newData
self.cacheVersion += 1 // 触发 UI 更新
}
}
}
}
最佳实践要点 #
- 识别变更来源:找出哪些操作会导致观察对象的属性变更
- 控制更新频率:避免在视图刷新期间修改观察对象的属性
- 利用 @ObservationIgnored:标记不需要触发 UI 更新的属性
- 实现断路机制:使用标志位或版本控制避免无限循环
- 分离计算逻辑:将重计算与 UI 更新隔离
- 异步处理:将耗时计算放在后台线程执行
解决方案选择指南 #
- 如果只需要隐藏特定属性的变更通知,使用 @ObservationIgnored
- 如果需要控制何时触发 UI 更新,使用版本控制机制
- 如果有复杂的数据处理逻辑,考虑分离数据模型与视图模型
- 如果需要定期刷新数据,实现后台定时更新
选择合适的方案取决于应用场景、性能要求和状态管理复杂度。通常,最佳解决方案是将这些策略组合使用,既保证数据的正确性,又避免 UI 性能问题。