SwiftUI计算属性引发的性能问题及优化咨询
解决SwiftUI预算应用计算属性性能问题的实践方案
1. 内存级自动缓存(替代持久化缓存)
放弃将计算结果存入SwiftData的方案,转而在模型内存层面维护缓存,通过监听交易数组变化自动清空缓存,触发重新计算。这种方式不会有数据不一致风险,且性能开销极低。
示例代码:
class MonthBudget: ObservableObject { @Published var transactions: [Transaction] = [] // 私有缓存存储 private var _totalSpent: Double? private var _spentNeedsBudget: Double? // 带缓存的计算属性 var totalSpent: Double { _totalSpent ?? calculateTotalSpent() } var spentNeedsBudget: Double { _spentNeedsBudget ?? calculateSpentNeeds() } private func calculateTotalSpent() -> Double { let value = transactions.reduce(0) { $0 + $1.amount } _totalSpent = value return value } private func calculateSpentNeeds() -> Double { let value = transactions.filter { $0.category == .needs }.reduce(0) { $0 + $1.amount } _spentNeedsBudget = value return value } // 监听交易数组变化,清空所有缓存 private var cancellables = Set<AnyCancellable>() init() { $transactions .sink { [weak self] _ in self?._totalSpent = nil self?._spentNeedsBudget = nil } .store(in: &cancellables) } }
2. 后台异步计算+主线程更新
将耗时的计算逻辑移到后台队列执行,完成后切回主线程更新UI状态,避免阻塞主线程导致卡顿。配合防抖处理,还能避免频繁修改交易时重复计算。
示例代码:
class MonthBudget: ObservableObject { @Published var transactions: [Transaction] = [] @Published var totalSpent: Double = 0 @Published var spentNeedsBudget: Double = 0 private var cancellables = Set<AnyCancellable>() init() { $transactions .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main) .receive(on: DispatchQueue.global(qos: .background)) .map { transactions in let total = transactions.reduce(0) { $0 + $1.amount } let needs = transactions.filter { $0.category == .needs }.reduce(0) { $0 + $1.amount } return (total, needs) } .receive(on: DispatchQueue.main) .sink { [weak self] total, needs in self?.totalSpent = total self?.spentNeedsBudget = needs } .store(in: &cancellables) } }
3. 按需计算,拆分逻辑
不要一次性计算所有统计值,而是根据当前视图展示的图表类型,只计算对应的数据。比如用户当前查看"需求类支出"图表,就仅计算spentNeedsBudget,其他统计值暂不处理。
实现方式:
- 在视图中用
@State标记当前活跃的图表类型(如.needs/.wants/.savings) - 模型根据传入的类型触发对应计算,避免不必要的遍历和累加操作
4. 预处理交易数据,按分类存储
在添加/删除交易时,直接将数据按预算分类(needs/wants/savings)拆分存储,而非每次计算时再过滤整个交易数组。这种方式能大幅减少计算阶段的耗时,尤其适合交易数量庞大的场景。
示例代码:
class MonthBudget: ObservableObject { @Published var allTransactions: [Transaction] = [] @Published var needsTransactions: [Transaction] = [] @Published var wantsTransactions: [Transaction] = [] func addTransaction(_ transaction: Transaction) { allTransactions.append(transaction) switch transaction.category { case .needs: needsTransactions.append(transaction) case .wants: wantsTransactions.append(transaction) case .savings: // 对应分类存储逻辑 } } func removeTransaction(_ transaction: Transaction) { allTransactions.removeAll { $0.id == transaction.id } needsTransactions.removeAll { $0.id == transaction.id } wantsTransactions.removeAll { $0.id == transaction.id } } // 直接使用分类数组计算,无需过滤 var spentNeedsBudget: Double { needsTransactions.reduce(0) { $0 + $1.amount } } }
5. 值类型配合自定义缓存包装器
如果MonthBudget使用值类型(struct),可以自定义@Memoized属性包装器来缓存计算结果,借助值类型的自动更新特性,在交易数组变化时自动失效缓存。
自定义包装器示例:
@propertyWrapper struct Memoized<Value> { private var cache: Value? private let compute: () -> Value init(compute: @escaping () -> Value) { self.compute = compute } var wrappedValue: Value { cache ?? { let value = compute() cache = value return value }() } mutating func invalidate() { cache = nil } }
模型中使用:
struct MonthBudget { var transactions: [Transaction] { didSet { _totalSpent.invalidate() _spentNeedsBudget.invalidate() } } @Memoized var totalSpent: Double { transactions.reduce(0) { $0 + $1.amount } } @Memoized var spentNeedsBudget: Double { transactions.filter { $0.category == .needs }.reduce(0) { $0 + $1.amount } } }
内容的提问来源于stack exchange,提问作者Louis
相关产品推荐
相关产品推荐

