iOS 16下SwiftUI的onAppear()失效导致崩溃问题咨询
问题核心
升级iOS16后,onAppear执行时机晚于视图渲染,导致selectedParcoursIndexArray还没完成填充就被Picker的Binding访问,直接触发数组越界崩溃。另外你当前的数组填充逻辑也存在问题——初始值设为[0],后续用append会导致数组长度比groupeManager.groupeList.count多1,逻辑上不匹配。
具体修复步骤
修正 GroupeManager 的状态管理与初始化逻辑
不要用@State存储GroupeManager,如果它是遵循ObservableObject的类,改用@StateObject来保证生命周期一致性;如果不是,也应该在视图初始化时先创建空实例,再在.task(iOS15+推荐,触发时机比onAppear更可靠)中加载数据。修改示例:
// 假设 GroupeManager 遵循 ObservableObject @StateObject var groupeManager: GroupeManager // 视图初始化时传入空构造(如果支持) init(courseId: String?) { _groupeManager = StateObject(wrappedValue: GroupeManager(courseId: courseId)) }同步数组长度,消除越界风险
把selectedParcoursIndexArray的填充逻辑改成直接生成对应长度的数组,替换初始值,确保和groupeManager.groupeList.count完全匹配:.task { // 若需要重新加载数据,这里更新groupeManager(建议用专门的加载方法而非直接重新初始化) groupeManager.reloadData(courseId: objCourse.id!) // 直接生成对应长度的默认数组 selectedParcoursIndexArray = Array(repeating: 0, count: groupeManager.groupeList.count) }给 Binding 加安全访问兜底
即使数组长度匹配,也建议在Binding的get/set方法中加入索引检查,避免极端情况崩溃:Picker(selection: Binding( get: { // 索引有效才返回对应值,否则返回默认0 self.selectedParcoursIndexArray.indices.contains(num) ? self.selectedParcoursIndexArray[num] : 0 }, set: { (newValue) in // 索引有效才赋值 if self.selectedParcoursIndexArray.indices.contains(num) { self.selectedParcoursIndexArray[num] = newValue peutSauvegarder = true } }), label: HStack{ Image(systemName: "person.fill") Text("\(groupeManager.groupeList[num].nomGroupe)") }.foregroundColor(selectedParcoursIndexArray.indices.contains(num) && selectedParcoursIndexArray[num] != 0 ? .orange : Color("ColorDarkLight")) ) { if enCourse.count != 0 { ... } }延迟渲染依赖数据的视图
在Form外层加条件判断,确保groupeManager.groupeList有数据后再渲染,彻底避免初期空数组的问题:if !groupeManager.groupeList.isEmpty { Form{ // 原Form内容 } } else { // 显示加载提示 ProgressView("加载中...") }
关键修改后的代码片段
struct DepartsSimultanesView: View { @EnvironmentObject var objCourse : CourseActuelle @EnvironmentObject var zindex : Zindex @EnvironmentObject var listActuelle : ListActuelle @State var selectedParcoursIndexArray : [Int] = [] @State var peutSauvegarder = false @StateObject var groupeManager: GroupeManager var listeParcoursRestants : [String] = [] @State private var action: Int? = 0 init(courseId: String?) { _groupeManager = StateObject(wrappedValue: GroupeManager(courseId: courseId)) } var body: some View { VStack{ HStack{ ... } HStack{ ... } // 数据加载完成后再渲染Form if !groupeManager.groupeList.isEmpty { Form{ Section(header: Text("Coureurs/Choix parcours")){ ForEach(0 ..< groupeManager.groupeList.count, id:\.self){ num in let detailManager = DetailManager(groupeId: groupeManager.groupeList[num].id, courseId: objCourse.id!) let listeParcoursRestants = listRestants(detailManager: detailManager) let enCourse = detailManager.enCourseList.filter { $0.groupeId == groupeManager.groupeList[num].id } Picker(selection: Binding( get: { self.selectedParcoursIndexArray.indices.contains(num) ? self.selectedParcoursIndexArray[num] : 0 }, set: { (newValue) in if self.selectedParcoursIndexArray.indices.contains(num) { self.selectedParcoursIndexArray[num] = newValue peutSauvegarder = true } }), label: HStack{ Image(systemName: "person.fill") Text("\(groupeManager.groupeList[num].nomGroupe)") }.foregroundColor(selectedParcoursIndexArray.indices.contains(num) && selectedParcoursIndexArray[num] != 0 ? .orange : Color("ColorDarkLight")) ) { if enCourse.count != 0 { ... } } .navigationBarHidden(true) .disabled(listeParcoursRestants.count == 1 || enCourse.count != 0) } } } } else { ProgressView("加载中...") } } .task { groupeManager.reloadData(courseId: objCourse.id!) selectedParcoursIndexArray = Array(repeating: 0, count: groupeManager.groupeList.count) } .navigationBarHidden(true) NavigationLink(destination: CestPartiView().environmentObject(objCourse), tag: 1, selection: $action){} } }
额外说明
iOS16对SwiftUI视图的渲染优先级和生命周期做了调整,onAppear可能会在视图部分渲染后才触发,所以依赖数据初始化的逻辑尽量用.task或者提前处理。另外,永远不要假设数组索引一定有效,加安全检查是避免崩溃的好习惯。
内容的提问来源于stack exchange,提问作者Clément Tengip

