Search code examples
iosswiftuser-interfaceswiftuiswiftdata

Swipe Action Modifier working inconsistently on Swift Data Model


I've been trying to create a Workout tracking app and been coming across a weird bug that appears in my app.

When using a "Mark as Favourite" .swipeActions modifier in a ForEach and on a NavigationLink, the workout is inconsistently marked as a "Favourite".

Here is the relevant WorkoutView code (within "var body: some View") :

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")
                    }
                }
            }
        }
        
        
    }

and here is the SwipeActionsAndAlertsModifiers file:

SwipeActionsAndAlertsModifiers modifier

struct SwipeActionsAndAlertsModifiers: ViewModifier {
    
    @Environment(\.modelContext) var modelContext
    @Bindable var workout: Workout
    
    // For Deleting
    @Binding var showingDeleteAlert: Bool
    @Binding var showDeleted: Bool
    
    // For Editing
    @Binding var editingWorkout: Bool
    
    // For Favouriting
    @Binding var markAsFavouriteAlert: Bool
    @Binding var unmarkAsFavouriteAlert: Bool
    
    func body(content: Content) -> some View {
        content
            // Deleting
            .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?")
            }
            
            // Favourited + Mark as Completed
            .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") // padding of 60%
                        .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?")
            }
        
    }
}

If any SwiftUI developer could let me know what the issue is / has the proficiency to debug it it would be much appreciated!! Let me know if more context explanation is required too. (The delete swipe action and alert works perfectly but the favouriting one doesn't, even when it is supposed(?) to be as simple as workout.favourites = true or false)

The above is what I have tried but to no avail so far!


Solution

  • Try this approach using a separate View for the NavigationLink and the modifier, as shown in this example code. This provides a specific workout to the View, works for me in my tests.

    struct LinkView: View {
        @State private var showingDeleteAlert = false
        @State private var showDeleted = false
        @State private var editingWorkout = false
        @State private var markAsFavouriteAlert = false
        @State private var unmarkAsFavouriteAlert = false
        
        var workout: Workout // <--- here
        
        var body: some View {
            NavigationLink {
                Text("destination")  // for testing
                //   WorkoutView(workout: workout, selectedTab: selectedTab, workoutTypes: workoutTypes, darkMode: darkMode)
            } label: {
                HStack {
                    Text(workout.name)
                    Text("\(workout.favourites)") // for testing
                }
                //  WorkoutsViewHStack(workout: workout, darkMode: darkMode)
            }
            .modifier(SwipeActionsAndAlertsModifiers(workout: workout, showingDeleteAlert: $showingDeleteAlert, showDeleted: $showDeleted, editingWorkout: $editingWorkout, markAsFavouriteAlert: $markAsFavouriteAlert, unmarkAsFavouriteAlert: $unmarkAsFavouriteAlert))
            
            .fullScreenCover(isPresented: $editingWorkout) {
                Text("fullScreenCover")  // for testing
                //  EditWorkoutView(workout: workout, workoutTypes: workoutTypes, selectedTab: $selectedTab, showingCompleted: $showingCompleted, showingLapsed: $showingLapsed, showingUpcoming: $showingUpcoming, darkMode: darkMode)
            }
        }
    }
    
    
    struct ContentView: View {
        // for testing
        @State private var searchedWorkouts: [Workout] = [
            Workout(name: "Mickey", favourites: false),
            Workout(name: "Mouse", favourites: false),
            Workout(name: "Donald", favourites: false),
            Workout(name: "Duck", favourites: false)
        ]
        
        @State private var searchQuery = ""
        
        var body: some View {
            NavigationStack {
                List {
                    ForEach(searchedWorkouts, id: \.id) { workout in
                        LinkView(workout: workout)
                    }
                }
                // .searchable(text: $searchQuery, prompt: "Search by keyword or date")
                // .overlay....
            }
        }
    }