SwiftUI: NavigationLink pops back when variable is updated
Asked Answered
C

1

1

I have this issue where the navigation link pops back when a variable is updated. It's a bit confusing because this behavior doesn't occur elsewhere in the application, but I am doing the same thing.

To start, there is a Vehicle struct that conforms to Identifiable. In content view, it's displayed in a NavigationView in a scrollview.

ScrollView(showsIndicators: false) {
            VStack {
                ForEach(user.vehicles) { vehicle in
                    VehicleListItem(user: $user,
                                    vehicle: vehicle)

                }
            }
        }

That list item has a navigation link that takes the user to a different view. Vehicle is passed as a binding to the next view.

NavigationLink(destination: {
                VehicleView(user: $user,
                            vehicle: $user.vehicles[user.vehicles.firstIndex(where: {$0.id == vehicle.id})!],
                            make: vehicle.make,
                            model: vehicle.model,
                            year: vehicle.year
                )
            }, label: {
                Image(systemName: "car")
                    .font(.system(size: 100))
            }).padding(.bottom, 20)

In Vehicle View, the user can edit the vehicles information (make, model, year) and it works fine. It doesn't pop back to the previous view when the user makes a change to the Vehicle.

Section() {
            VehicleHandler(make: $make, model: $model, year: $year)
            
            Button(action: {
                //Update
                vehicle.make = make
                vehicle.model = model
                vehicle.year = year
                
            }, label: {
                ButtonView(label: "Update")
            }).disabled(make.isEmpty ||
                        model.isEmpty ||
                        (make == vehicle.make && model == vehicle.model && year == vehicle.year))
        }

Vehicle Handler

struct VehicleHandler: View {

enum Field: Hashable {
    case make
    case model
}

@FocusState private var field: Field?

@Binding var make: String
@Binding var model: String
@Binding var year: Int //Set to current year

var body: some View {
    TextField("Make", text: $make)
        .submitLabel(.next)
        .focused($field, equals: .make)
        .onSubmit {
            field = .model
        }
    
    TextField("Model", text: $model)
        .submitLabel(.done)
        .focused($field, equals: .model)

    
    Picker("Year", selection: $year, content: {
        ForEach((1900...Date().year()).reversed(), id: \.self) { year in
            Text(String(year))
                .tag(year as Int)
        }

    })
}

}

Now, the user can add maintenance records. Once they've added a record, it's displayed using a ForEach. The records also conform to identifiable.

//Only show 5
            ForEach(vehicle.maintenanceRecords.prefix(5)) { record in
                NavigationLink(destination: {
                    MaintenanceView(user: $user,
                                    record: $vehicle.maintenanceRecords[vehicle.getMaintenanceIndex(id: record.id)],
                                    date: record.date,
                                    mileage: record.mileage ?? 0,
                                    note: record.note,
                                    cost: record.cost ?? 0,
                                    tasks: record.tasks)
                }, label: {
                    Text("\(record.date.formattedNoYear()) - \(record.note)")
                })
            }

The same concept is being used to edit the maintenance record. Record is passed as a binding, and the record information is passed to update the various fields. When the user presses update, it updates the record. It's at this point where it will pop back to VehicleView anytime you make an edit and press Update. I used to have this issue when I didn't use IDs, however, every struct conforms to Identifiable and the IDs aren't being modified. I've checked to ensure they stay the same, and that includes the IDs for the Vehicle, and Record. Does anyone have any idea why it keeps popping back here, but it doesn't when the Vehicle information is updated? Let me know if you need more information.

            RecordHandler(user: $user,
                      date: $date,
                      mileage: $mileage,
                      note: $note,
                      cost: $cost,
                      task: $task,
                      tasks: $tasks)
        
        Section() {
            Button(action: {
                
                //Update record
                record.date = date
                record.note = note
                record.tasks = tasks
                
                if mileage.isZero == false {
                    record.mileage = mileage
                }
                
                if cost.isZero == false {
                    record.cost = cost
                }
                
                //Clear task
                task = ""
            }, label: {
                ButtonView(label: "Update")
            }).disabled(note.isEmpty ||
                        (date == record.date && mileage == record.mileage && note == record.note && cost == record.cost && tasks == record.tasks)
            )
        }

Record Handler

struct RecordHandler: View {

enum Field: Hashable {
    case note
    case mileage
    case cost
    case task
}

@FocusState var field: Field?

@Binding var user: User
@Binding var date: Date
@Binding var mileage: Float
@Binding var note: String
@Binding var cost: Float
@Binding var task: String
@Binding var tasks: [Vehicle.Record.Task]

@State var suggestion: String = ""

var body: some View {
    Section() {
        DatePicker("Date", selection: $date)
        TextField("Note", text: $note)
            .submitLabel(.next)
            .focused($field, equals: .note)
            .onSubmit {
                field = .mileage
            }
        FieldWithLabel(label: "Mileage", value: $mileage, formatter: NumberFormatter.number)
            .submitLabel(.next)
            .focused($field, equals: .mileage)
            .onSubmit {
                field = .cost
            }
        FieldWithLabel(label: "Cost", value: $cost, formatter: NumberFormatter.currency)
            .submitLabel(.next)
            .focused($field, equals: .cost)
            .onSubmit {
                field = .task
            }
    }
    
    //For task
    Section() {
        HStack {
            TextField("Task", text: $task)
                .submitLabel(.done)
                .focused($field, equals: .task)
                .onSubmit {
                    addTask()
                    field = .task
                }
                .toolbar(content: {
                    ToolbarItemGroup(placement: .keyboard) {
                        Spacer()

                        Button("Close") {
                            UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
                        }
                    }
                })
                .onChange(of: task, perform: { value in
                    //See if any of the saved task are similar
                    if user.tasks.contains(where: {$0.contains(value)}) {
                        //Set suggestion
                        suggestion = user.tasks.first(where: {$0.contains(value)})!
                    } else {
                        //Leave it empty if there's nothing
                        suggestion = ""
                    }
                })
            Spacer()
            
            Button(action: {
                addTask()
            }, label: {
                Image(systemName: "plus.circle.fill")
                    .font(.system(size: 22))
            }).buttonStyle(BorderlessButtonStyle())
            
        }
        
        //To show task suggestions
        if suggestion.isEmpty == false {
            Button(action: {
                //Set task to suggestion
                task = suggestion
                
                //Add task
                addTask()
                
                //Focus field
                field = .task
            }, label: {
                HStack {
                    Text("Suggestion")
                    Spacer()
                    Text(suggestion)
                }
            })
        }

        
        ForEach(tasks) { task in
            Text(task.name)
        }
        .onDelete(perform: delete)
    }

}

func addTask() {
    //Create
    let task = Vehicle.Record.Task(name: task)
    
    //Add to array
    tasks.append(task)
    
    //Clear
    self.task = ""
}

func delete(at offsets: IndexSet) {
    tasks.remove(atOffsets: offsets)
}

}

Ciel answered 9/12, 2021 at 14:9 Comment(1)
Welcome to SO - Please take the tour and read How to Ask to improve, edit and format your questions. Without a Minimal Reproducible Example it is impossible to help you troubleshoot.Testes
F
9

I had the same problem and managed to solve it today. Try adding the option .navigationViewStyle(.stack) to your NavigationView

struct ContentView: View {
    var body: some View {
        NavigationView {
            //All your content and navigation links
        }.navigationViewStyle(.stack)
    }
}
Feltie answered 10/12, 2021 at 16:1 Comment(3)
So I had it set to that, however, I had it applied on the list inside the navigation view instead of the navigation view itself. Once I moved it, it started working.Ciel
For me wrapping the content in a NavigationView solved it. Thanks for the tip. Did not need the .navigationViewStyle(.stack).Margemargeaux
.navigationViewStyle(.stack) is deprecated, replace the NavigationView with NavigationStack also worksSoutheastwards

© 2022 - 2025 — McMap. All rights reserved.