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

Swift Data模型下Swipe Action Modifier收藏标记功能异常排查

健身追踪App收藏标记Bug排查:ForEach+NavigationLink下SwipeActions状态不一致

问题核心

开发健身追踪App时,列表项通过ForEach生成并嵌套NavigationLink,使用自定义SwipeActionsAndAlertsModifiers实现收藏/取消收藏滑动操作,出现收藏状态更新不一致的问题:删除功能完全正常,但点击收藏弹窗的确认按钮后,UI不更新或状态混乱。

相关代码

Workouts View 核心代码

List {
    ForEach(searchedWorkouts, id: \.id) { workout in
        NavigationLink {
            WorkoutView(workout: workout, selectedTab: selectedTab, workoutTypes: workoutTypes, darkMode: darkMode)
        } label: {
            WorkoutsViewHStack(workout: workout, darkMode: darkMode)
        }
        .modifier(SwipeActionsAndAlertsModifiers(workout: workout, showingDeleteAlert: $showingDeleteAlert, showDeleted: $showDeleted, editingWorkout: $editingWorkout, markAsFavouriteAlert: $markAsFavouriteAlert, unmarkAsFavouriteAlert: $unmarkAsFavouriteAlert))
        .fullScreenCover(isPresented: $editingWorkout) {
            EditWorkoutView(workout: workout, workoutTypes: workoutTypes, selectedTab: $selectedTab, showingCompleted: $showingCompleted, showingLapsed: $showingLapsed, showingUpcoming: $showingUpcoming, darkMode: darkMode)
        }
    } 
} 
.searchable(text: $searchQuery, prompt: "Search by keyword or date")
.overlay {
    if searchedWorkouts.isEmpty && searchQuery.isEmpty == false  {
        ContentUnavailableView {
            Label("No results", systemImage: "magnifyingglass")
                .padding()
            
            Text("Check the spelling or try a new search.")
                .font(.subheadline)
        }
        
    } else if searchedWorkouts.isEmpty {
        ContentUnavailableView {
            Label(filterType == "All" ? "No workouts found": "\(filterType) workouts weren't found :( ", systemImage: "note")
                .padding()
        } actions: {
            Button {
                showingAddWorkout.toggle()
            } label: {
                Label("Add a new workout", systemImage: "plus.square")
            }
        }
    }
}

SwipeActionsAndAlertsModifiers 代码

struct SwipeActionsAndAlertsModifiers: ViewModifier {
    @Environment(\.modelContext) var modelContext
    @Bindable var workout: Workout
    
    @Binding var showingDeleteAlert: Bool
    @Binding var showDeleted: Bool
    @Binding var editingWorkout: Bool
    @Binding var markAsFavouriteAlert: Bool
    @Binding var unmarkAsFavouriteAlert: Bool
    
    func body(content: Content) -> some View {
        content
            // 删除操作
            .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                Button {
                    showingDeleteAlert.toggle()
                } label: {
                    Image("icons8-delete-darkmode")
                        .resizable()
                        .frame(width: 28, height: 28)
                        .scaledToFit()
                        .tint(.red)
                }
            }
            .alert("Delete Workout", isPresented: $showingDeleteAlert) {
                Button("Delete", role: .destructive) {
                    modelContext.delete(workout)
                    showDeleted.toggle()
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("Are you sure you want to delete this workout permanently?")
            }
            
            // 收藏操作
            .swipeActions(edge: .leading, allowsFullSwipe: false) {
                Button {
                    if workout.favourites == false {
                        markAsFavouriteAlert.toggle()
                    } else {
                        unmarkAsFavouriteAlert.toggle()
                    }
                } label: {
                    workout.favourites == false ?
                    Image(systemName: "heart.fill")
                        .resizable()
                        .frame(width: 5, height: 5)
                        .scaledToFit()
                        .tint(.yellow) :
                    Image(systemName: "heart.slash.fill")
                        .resizable()
                        .frame(width: 5, height: 5)
                        .scaledToFit()
                        .tint(.yellow)
                }
            }
            .alert("Mark as Favourite", isPresented: $markAsFavouriteAlert) {
                Button("Mark as Favourite", action: { workout.favourites = true })
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("Mark this workout as a Favourite?")
            }
            .alert("Unmark as Favourite", isPresented: $unmarkAsFavouriteAlert) {
                Button("Unmark as Favourite", action: { workout.favourites = false })
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("Unmark this workout as a Favourite?")
            }
    }
}

问题原因分析

  1. 共享弹窗Binding变量冲突:所有列表项共用同一个markAsFavouriteAlert和unmarkAsFavouriteAlert绑定,当多个列表项触发弹窗时,状态会互相干扰,导致弹窗对应的Workout实例混乱,最终更新错误的对象状态。
  2. SwiftData状态未持久化:修改workout.favourites后未手动调用modelContext.save(),SwiftData对于属性修改不会自动触发保存(删除操作会自动触发),导致状态未同步到持久层,UI刷新异常。
  3. UI元素尺寸问题:收藏按钮的图片尺寸设为width:5, height:5,几乎不可见,可能导致用户误操作,误以为功能失效。

修复方案

1. 为每个列表项独立管理弹窗状态

将Modifier中的弹窗Binding改为内部@State变量,避免全局状态冲突:

struct SwipeActionsAndAlertsModifiers: ViewModifier {
    @Environment(\.modelContext) var modelContext
    @Bindable var workout: Workout
    
    @Binding var showingDeleteAlert: Bool
    @Binding var showDeleted: Bool
    @Binding var editingWorkout: Bool
    
    // 改为内部@State,每个Modifier实例独立管理弹窗
    @State private var markAsFavouriteAlert = false
    @State private var unmarkAsFavouriteAlert = false
    
    func body(content: Content) -> some View {
        content
            // 删除操作代码不变
            .swipeActions(edge: .trailing, allowsFullSwipe: false) {
                Button {
                    showingDeleteAlert.toggle()
                } label: {
                    Image("icons8-delete-darkmode")
                        .resizable()
                        .frame(width: 28, height: 28)
                        .scaledToFit()
                        .tint(.red)
                }
            }
            .alert("Delete Workout", isPresented: $showingDeleteAlert) {
                Button("Delete", role: .destructive) {
                    modelContext.delete(workout)
                    showDeleted.toggle()
                    try? modelContext.save() // 确保删除后保存
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("Are you sure you want to delete this workout permanently?")
            }
            
            // 收藏操作优化
            .swipeActions(edge: .leading, allowsFullSwipe: false) {
                Button {
                    workout.favourites ? unmarkAsFavouriteAlert.toggle() : markAsFavouriteAlert.toggle()
                } label: {
                    Image(systemName: workout.favourites ? "heart.slash.fill" : "heart.fill")
                        .resizable()
                        .frame(width: 20, height: 20) // 调整为可见尺寸
                        .tint(.yellow)
                }
            }
            .alert("Mark as Favourite", isPresented: $markAsFavouriteAlert) {
                Button("Mark as Favourite") {
                    workout.favourites = true
                    try? modelContext.save() // 手动保存SwiftData状态
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("Mark this workout as a Favourite?")
            }
            .alert("Unmark as Favourite", isPresented: $unmarkAsFavouriteAlert) {
                Button("Unmark as Favourite") {
                    workout.favourites = false
                    try? modelContext.save() // 手动保存SwiftData状态
                }
                Button("Cancel", role: .cancel) {}
            } message: {
                Text("Unmark this workout as a Favourite?")
            }
    }
}

2. 调整Workouts View代码

移除原来的全局markAsFavouriteAlert和unmarkAsFavouriteAlert变量,因为现在Modifier内部独立管理这些状态:

// 删除这两个全局变量
// @State private var markAsFavouriteAlert = false
// @State private var unmarkAsFavouriteAlert = false

// 修改Modifier调用,去掉对应的Binding参数
.modifier(SwipeActionsAndAlertsModifiers(workout: workout, showingDeleteAlert: $showingDeleteAlert, showDeleted: $showDeleted, editingWorkout: $editingWorkout))

3. 确保Workout模型正确配置

如果使用SwiftData,确保模型定义正确:

@Model
class Workout {
    var id: UUID
    var favourites: Bool
    // 其他属性...
    
    init(id: UUID = UUID(), favourites: Bool = false) {
        self.id = id
        self.favourites = favourites
    }
}

如果使用ObservableObject,需为favourites添加@Published属性:

class Workout: ObservableObject {
    let id: UUID
    @Published var favourites: Bool
    // 其他属性...
    
    init(id: UUID = UUID(), favourites: Bool = false) {
        self.id = id
        self.favourites = favourites
    }
}

验证要点

  • 测试单个列表项的收藏/取消收藏操作,确认UI实时更新
  • 测试多个列表项交替操作,确保状态互不干扰
  • 重启App后检查收藏状态是否持久化

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 17:34:55