Joseph Kessler
Joseph Kessler

Reputation: 133

SwiftUI: NavigationLink pops back when variable is updated

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

}

Upvotes: 1

Views: 2437

Answers (1)

Thorin
Thorin

Reputation: 437

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

Upvotes: 11

Related Questions