skip to Main Content

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

2

Answers


  1. 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.

    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search