Modal Data — 观察者模式引起的无限递归问题
This article is extracted from the chat log with AI. Please identify it with caution.

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 性能问题。

本文共 1088 字,创建于 Apr 28, 2025
相关标签: Xcode, SwiftUI, ByAI