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") :
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:
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!
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....
}
}
}