SwiftUI中检测Workout的Exercise数组变化并更新NavigationPath
问题描述
我在AddWorkoutView中用@State存储SwiftData模型Workout:
@State private var workout: Workout = Workout(id: UUID())
该模型包含Exercise模型的数组。我通过弹窗向Workout添加Exercise后关闭弹窗:
let newExercise = Exercise(id: UUID(),exerciseName: selection!, date: Date.now) modelContext.insert(newExercise) // 解包workout并写入 if let workout = workout { workout.exercises?.append(newExercise) } dismiss()
当前导航栈实现如下,需求是添加完Exercise后直接打开EditExerciseView,无需用户手动点击:
NavigationStack(path: $router.path) { VStack { List { Section(header: Text("Workout Details")) { TextField("Enter workout name",text: $workoutName) DatePicker("Start Time", selection: $workoutStartTime) DatePicker("End Time", selection: $workoutEndTime) } Section(header: Text("Exercises")) { if let exercises = workout.exercises { ForEach(exercises){ exercise in NavigationLink(value: exercise) { Text(exercise.exerciseName.name) .padding(.top, 5) .padding(.bottom, 5) } } .onDelete(perform: delete) } else { Text("No Exericses Yet") } } Section { Button { showExercieSheet = true } label: { Text("Add Exercise") } } } .navigationDestination(for: Exercise.self) { exercise in EditExeriseView(exercise: exercise, sets: exercise.sets ?? []) .navigationBarBackButtonHidden(true) } .listStyle(.insetGrouped) } } .toolbar { ToolbarItem(placement: .topBarLeading) { Button{ showCancelAlert.toggle() }label: { Text("Cancel") .foregroundStyle(.red) .underline() } } ToolbarItem(placement: .principal) { Text(workoutName) .fontWeight(.semibold) } ToolbarItem(placement: .navigationBarTrailing) { Button { // 保存内容(所有字段可选,但要求填写名称) if(!workoutName.isEmpty) { workout.name = workoutName workout.startTime = workoutStartTime workout.endTime = workoutEndTime modelContext.insert(workout) dismiss() } } label: { Text("Save") .font(.headline) .underline() .foregroundStyle(!workoutName.isEmpty ? .blue : .gray) .fontWeight(.semibold) } } } .sheet(isPresented: $showExercieSheet) { AddExerciseView(workout: workout) .presentationDragIndicator(.visible) }
想问:SwiftUI有没有办法检测Workout的Exercise数组新增元素,自动将其加入全局NavigationPath?
解决方案
方法一:通过Sheet回调直接传递新增Exercise
这是最可靠的方案,无需依赖数组监听,直接在创建完成后手动更新导航路径:
- 修改
AddExerciseView,添加回调闭包传递新增的Exercise:
struct AddExerciseView: View { let workout: Workout var onExerciseAdded: ((Exercise) -> Void)? // 新增回调 // ... 原有代码 ... // 创建并添加Exercise时调用回调 let newExercise = Exercise(id: UUID(),exerciseName: selection!, date: Date.now) modelContext.insert(newExercise) if let workout = workout { workout.exercises?.append(newExercise) } onExerciseAdded?(newExercise) // 传递新增元素 dismiss() }
- 在
AddWorkoutView中打开Sheet时传入回调,更新导航路径:
.sheet(isPresented: $showExercieSheet) { AddExerciseView(workout: workout) { newExercise in router.path.append(newExercise) // 直接加入导航路径 } .presentationDragIndicator(.visible) }
方法二:监听Workout的Exercises数组变化
如果需要通过监听数组变化实现,可以用onChange监听数组更新:
// 在AddWorkoutView的NavigationStack内添加 .onChange(of: workout.exercises) { oldValue, newValue in // 对比新旧数组,找到新增的最后一个元素 if let newExercise = newValue?.last, oldValue?.contains(newExercise) == false { router.path.append(newExercise) } }
注意:此方法需要Exercise模型遵循Equatable,否则contains判断会失效:
@Model class Exercise: Equatable { static func == (lhs: Exercise, rhs: Exercise) -> Bool { lhs.id == rhs.id // 用唯一ID判断相等 } var id: UUID var exerciseName: ... var date: Date // ... 其他属性 ... init(id: UUID, exerciseName: ..., date: Date) { self.id = id self.exerciseName = exerciseName self.date = date } }
关键提示
- 方法一更推荐,避免了数组监听可能带来的时序问题
- 使用方法二时,必须确保
Exercise正确实现Equatable - 导航路由
router需要是全局可访问的状态(比如用@StateObject管理的路由类)
内容的提问来源于stack exchange,提问作者Milan Labus
相关产品推荐
相关产品推荐

