4

I am new to Swift and SwiftUI and working on an app that requires passing nested bindings through various views. I'm encountering an issue where a NavigationLink in a subview passing a binding to a child detail view causes the app to completely freeze both when tested on a real device and simulator. However, it does not freeze within the SwiftUI preview canvas.

I created a thinned down project to test this and the issue persists. I also tried presenting the child detail view via a sheet but the binding does not update within the sheet view.

Can anyone see if there is anything obviously wrong that is causing the issue?

The NavigationLink in the ParentDetail view causes the freeze. Here is the sample code:

Basic Models:

import SwiftUI

class ParentStore: ObservableObject {
    @Published var parents = [ParentObject.parentExample]
    
    func binding(for parentID: UUID) -> Binding<ParentObject> {
        Binding {
            guard let index = self.parents.firstIndex(where: { $0.id == parentID }) else {
                fatalError()
            }
            
            return self.parents[index]
        } set: { updatedParent in
            guard let index = self.parents.firstIndex(where: { $0.id == parentID}) else {
                fatalError()
            }
            return self.parents[index] = updatedParent
        }
    }

}

struct ParentObject: Identifiable {
    var id = UUID()
    var name: String
    var children: [Child]
    
    static let parentExample = ParentObject(name: "Matt", children: [.sasha, .brody])
    static let emptyParent = ParentObject(name: "Empty", children: [])
}

struct Child: Identifiable {
    var id = UUID()
    var name: String
    var grandkids: [Grandkid]
    
    static let sasha = Child(name: "Sasha", grandkids: [.peter, .meagan])
    static let brody = Child(name: "Brody", grandkids: [.michelle])
}

struct Grandkid: Identifiable {
    var id = UUID()
    var name: String
    
    static let peter = Grandkid(name: "Peter")
    static let meagan = Grandkid(name: "Meagan")
    static let michelle = Grandkid(name: "Michelle")
}

MainView:

struct ContentView: View {
    @StateObject var parentStore = ParentStore()
    
    var body: some View {
        TabView {
            ParentList()
                .environmentObject(parentStore)
                .tabItem {
                    Label("List", systemImage: "list.bullet")
                }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

ParentList:

struct ParentList: View {
    @EnvironmentObject var parentStore: ParentStore
    
    var body: some View {
        NavigationStack {
            List {
                ForEach($parentStore.parents) { $parent in
                    NavigationLink(parent.name, value: parent.id)
                }
            }
            .navigationDestination(for: UUID.self) { parentID in
                ParentDetail(parent: parentStore.binding(for: parentID))
            }
        }
    }
}

struct ParentList_Previews: PreviewProvider {
    static var previews: some View {
        ParentList()
            .environmentObject(ParentStore())
    }
}

ParentDetailView:

struct ParentDetail: View {
    @Binding var parent: ParentObject
    @State private var child: Binding<Child>?
    
    var body: some View {
        List {
            Section("Parent") {
                Text(parent.name)
            }
            
            Section("Children") {
                ForEach($parent.children) { $child in
// This navigation link causes the freeze
                    NavigationLink(child.name) {
                        ChildDetail(child: $child)
                    }
                    // Testing sheet presentation...
                    Button(child.name) {
                        self.child = $child
                    }

                }
            }
        }
        .sheet(item: $child) { $child in
            ChildDetail(child: $child)
        }
    }
}

struct ParentDetail_Previews: PreviewProvider {
    static var previews: some View {
        ParentDetail(parent: .constant(.parentExample))
    }
}


Child Detail View:

struct ChildDetail: View {
    @Binding var child: Child
    
    var body: some View {
        VStack {
            TextField("Name", text: $child.name)
            
            ForEach(child.grandkids) { grandkid in
                Text(grandkid.name)
            }
        }
    }
}

struct ChildDetail_Previews: PreviewProvider {
    static var previews: some View {
        ChildDetail(child: .constant(.sasha))
    }
}
7
  • Don’t use that terrible binding solution. It loop through all your items at least twice every time the view is redrawn. Apple has not provided a way to use Binding with navigationDestination, use NavigationLink that includes destination. I know that solution is all over the place but it is terribly inefficient and causes various bugs. Apr 27, 2023 at 17:37
  • Also you can have a State or a Binding but you can’t have a State that is a Binding. State is a source of truth that comes with Binding built in. Apr 27, 2023 at 17:38
  • You should share a store somehow so you can keep a single source of truth for different updates. Also for complex models like this is why you should use ObservableObjects as models, they simplify the passing around since you are dealing with reference types. developer.apple.com/documentation/swiftui/… Apr 27, 2023 at 17:43
  • The binding solution came from Apple's own FoodTruck example. I've just tried passing the parent detail view the UUID from the list and added a computed property to pull the binding from the model and it seems to allow the navigation link on the detail page to work -I'm not sure why this works... What method would you have used for giving a child view access to a binding while using the newer navigation destination modifier?
    – mmmmmatt
    Apr 27, 2023 at 19:28
  • I have the store shared via injecting it in as an environment object in the top-level view and am using the @EnvironmentObject wrapper to access it in child views (not shown in this example as it wasn't causing the NavigationLink issues.)
    – mmmmmatt
    Apr 27, 2023 at 19:32

3 Answers 3

1

This is an ongoing problem that I am also facing. I am new to posting so my apologies if I've left out something important.

In the below code, there is a NavigationStack in my root view and all other child views contain NavigationLinks. I found I could not use the NavigationLink(value:destination:) form with a .navigationDestination since freezing would occur with a Binding as well. I have reduced this code to the minimum to demonstrate the freezing that occurs only with a Binding within my destination view of the NavigationLink.

Here is my common code (only relevant fields included):

struct Patient: Identifiable {
   var id: Int = 0  // database identifier
   var name: String = "None"
}

struct Patients: ObservableObject {
  @Published var items: [Patient] = []  // loaded from database
}

struct StateController: ObservableObject {
  @Published var patients: Patients()
}

Here is the code using a Binding that causes indefinite freezing (and a recursive loop but no errors):

struct TempPatient: View {
  @Binding var patient: Patient

  var body: some View {
    Text("Patient name: \(patient.name)")
  }
}

List {
  ForEach(listing) { pt in
    NavigationLink {
        TempPatient(patient: $stateController.patients.items[0])  // causes infinite recursion but no error messages
    } label: {
        PatientRow(patient: pt)
    }
  }
  .onDelete(perform: delete(indexSet:))

}

The next code works, with the only change being not using a Binding for the patient:

struct TempPatient: View {
  @State var patient: Patient

  var body: some View {
    Text("Patient name: \(patient.name)")
  }
 }

List {
  ForEach(listing) { pt in
    NavigationLink {
      TempPatient(patient: stateController.patients.items[0])   // works to show TempPatient view
    } label: {
        PatientRow(patient: pt)
    }
  }
  .onDelete(perform: delete(indexSet:))

}

So the problem seems to be with NavigationLink and a Binding within the destination view. It did not happen with NavigationView but surfaced only when using the combination of a NavigationStack and NavigationLink.

2
  • You could edit your answer and point out where and how your code is a (partial?) solution to the question.
    – soundflix
    Aug 21, 2023 at 11:30
  • This does not really answer the question. If you have a different question, you can ask it by clicking Ask Question. To get notified when this question gets new answers, you can follow this question. Once you have enough reputation, you can also add a bounty to draw more attention to this question. - From Review
    – soundflix
    Aug 21, 2023 at 11:30
0

You need to replace this:

NavigationLink(child.name) {
    ChildDetail(child: $child)
}

With the same value/destination new API as you used in the parent because I don't think you can use it along with the old View based version, e.g.

NavigationLink(child.name, value: child.id)

.navigationDestination(for: Child.ID.self) { childID in
    ChildDetail(parent: store.binding(for: childID))
}

Better make this edit too:

.navigationDestination(for: Parent.ID.self) { parentID in

I would also recommend re-working your data model to have a Person struct instead of Parent and Child which are essentially the same thing. Then you can store the child ID to ID relations in an array. We have to organise things differently when using value types for data instead of objects.

2
  • I made your updates to the parent navDestination and added the navDestination(Child.ID.self) for the Child NavLink and the app is still freezing when using the navigation link for the ChildDetail view. I am also getting the warning: "A navigationDestination for “Foundation.UUID” was declared earlier on the stack. Only the destination declared closest to the root view of the stack will be used."
    – mmmmmatt
    May 1, 2023 at 19:32
  • I'm not sure how I'd rework the data model as you mention. In the larger app I am working on, each Parent struct owns a unique array of Children. The structs are also a bit different. The Parent in the real app is a workout and its "children" are the exercises unique to the workout.
    – mmmmmatt
    May 1, 2023 at 19:36
0

I had the same issue. In the end I tried to replace NavigationStack with NavigationView and the issue was solved. Also you have to set proper navigation link behaviour using .isDetailLink(false)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.