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

SwiftUI:如何为计算属性实现@State等效功能以修复导航异常?

解决NavigationView导航栈异常:计算属性替代@State存储属性的方案

问题背景

现有SwiftUI代码中,ServicesView使用普通存储属性elements作为List数据源时,会出现异常:当App进入非活跃状态且已展示YearView时,YearView会被弹出导航栈。将elements改为@State修饰的存储属性后,问题消失,但实际需求中elements需要是基于@FetchRequest获取的Month数据分组生成的计算属性,而@State无法修饰计算属性。

原基础代码:

struct Element: Identifiable {
    let id = UUID()
    let text: String
    init(text: String) {
        self.text = text
    }
}

struct ServicesView: View {
    @FetchRequest(sortDescriptors: [SortDescriptor(\.date, order: .reverse)], animation: .easeIn)
    private var results: FetchedResults<Month>
    var elements = [Element(text: "a"), Element(text: "b")]
    var body: some View {
        NavigationView {
            List(elements) { element in
                NavigationLink {
                    YearView(months: [])
                } label: {
                    Text("abc")
                }
            }
            .navigationTitle("Service")
        }
    }
}

实际需求的分组计算属性代码:

struct Group: Identifiable { // 替代上述Element
    let months: [Month]
    var id = UUID()
    var descriptiveYear: String {
        months.first?.descriptiveYear ?? ""
    }
    init(months: [Month]) {
        self.months = months
    }
}

private var elements: [Group] {
    var groups = [[Month]]()
    var months = [Month]()
    results.forEach { month in
        guard months.isEmpty else {
            if month.currentYear == months.first?.currentYear {
                months.append(month)
            } else {
                groups.append(months)
                months = [month]
            }
            return
        }
        months.append(month)
    }
    return groups.map { Group(months: $0) }
}

问题根源

普通计算属性每次View刷新都会重新执行分组逻辑,生成全新的Group数组,每个Group的UUID也会重新生成。List依赖元素的id来跟踪列表项状态,NavigationLink的导航栈状态也和列表项的id绑定。频繁变化的id会导致NavigationView认为列表项已被替换,从而重置导航栈,弹出YearView。而@State存储属性会保留数组的引用,不会每次刷新都重新生成元素,id保持稳定,因此导航栈状态正常。

解决方案

方案1:用@StateObject封装分组逻辑

创建一个ObservableObject类,负责监听results的变化,处理分组并维护稳定的groups数组,只有当分组内容真正变化时才更新,避免不必要的元素重建。

class GroupViewModel: ObservableObject {
    @Published var groups: [Group] = []
    
    func updateGroups(with results: FetchedResults<Month>) {
        var tempGroups = [[Month]]()
        var currentMonths = [Month]()
        
        results.forEach { month in
            guard !currentMonths.isEmpty else {
                currentMonths.append(month)
                return
            }
            
            if month.currentYear == currentMonths.first?.currentYear {
                currentMonths.append(month)
            } else {
                tempGroups.append(currentMonths)
                currentMonths = [month]
            }
        }
        
        if !currentMonths.isEmpty {
            tempGroups.append(currentMonths)
        }
        
        // 仅当分组内容变化时更新,避免不必要的刷新
        let newGroups = tempGroups.map { Group(months: $0) }
        if newGroups != groups {
            groups = newGroups
        }
    }
}

struct ServicesView: View {
    @FetchRequest(sortDescriptors: [SortDescriptor(\.date, order: .reverse)], animation: .easeIn)
    private var results: FetchedResults<Month>
    
    @StateObject private var groupVM = GroupViewModel()
    
    var body: some View {
        NavigationView {
            List(groupVM.groups) { group in
                NavigationLink {
                    YearView(months: group.months)
                } label: {
                    Text(group.descriptiveYear)
                }
            }
            .navigationTitle("Service")
            .onChange(of: results) { newResults in
                groupVM.updateGroups(with: newResults)
            }
        }
        .onAppear {
            groupVM.updateGroups(with: results)
        }
    }
}

方案2:让Group的id基于稳定的分组标识

修改Group的id实现,用分组内的稳定特征(比如年份)作为id,而不是每次生成新的UUID。这样即使计算属性重新生成Group,同一分组的id也保持不变,NavigationLink的标识稳定。

struct Group: Identifiable {
    let months: [Month]
    // 用分组的年份作为id,确保同一分组的id稳定不变
    var id: String {
        months.first?.currentYear ?? UUID().uuidString
    }
    var descriptiveYear: String {
        months.first?.descriptiveYear ?? ""
    }
    init(months: [Month]) {
        self.months = months
    }
}

// 原计算属性逻辑保持不变
private var elements: [Group] {
    var groups = [[Month]]()
    var months = [Month]()
    results.forEach { month in
        guard months.isEmpty else {
            if month.currentYear == months.first?.currentYear {
                months.append(month)
            } else {
                groups.append(months)
                months = [month]
            }
            return
        }
        months.append(month)
    }
    if !months.isEmpty {
        groups.append(months)
    }
    return groups.map { Group(months: $0) }
}

方案3:用@Memoized缓存计算属性结果(Swift 5.9+)

如果项目使用Swift 5.9及以上版本,可以使用@Memoized宏缓存计算属性的结果,避免每次View刷新都重新执行分组逻辑,保持Group数组的稳定性。

// 需要导入必要的宏库
import SwiftSyntaxMacros
import SwiftCompilerPlugin

@Memoized
private var elements: [Group] {
    var groups = [[Month]]()
    var months = [Month]()
    results.forEach { month in
        guard months.isEmpty else {
            if month.currentYear == months.first?.currentYear {
                months.append(month)
            } else {
                groups.append(months)
                months = [month]
            }
            return
        }
        months.append(month)
    }
    if !months.isEmpty {
        groups.append(months)
    }
    return groups.map { Group(months: $0) }
}

方案对比

  • 方案1:逻辑清晰,完全控制更新时机,适合复杂分组场景,唯一缺点是需要额外编写ViewModel类。
  • 方案2:代码改动最小,仅修改Group的id实现,适合分组有稳定标识的简单场景。
  • 方案3:对原有代码入侵性最低,但依赖Swift版本,适合使用最新Swift环境的项目。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 13:01:05