Search code examples
swiftswiftuiswiftui-navigationlink

How to use a NavigationLink inside a trailing closure of a view?


I have List like below,

struct ListItemView: View {
    let vm: ListItemViewModel
    var callHandler: ((ListItemViewModel) -> Void)?
    var textHandler: ((ListItemViewModel) -> Void)?

    HStack {
       Text("Click Me")
    }

}
.swipeActions(allowsFullSwipe: false) {
    
        Button {
            textAction()
        } label: {
            Label("Go", systemImage: "text.bubble.fill")
        }
        .tint(.green)

}

private extension PhoneItemView {

    var callAction: () -> Void {
        return {
            callHandler?(vm)
        }
}

I'm trying to figure out how to trigger a NavigationLink when swipe action is pressed. My ContentView looks like this.

 List{
        ForEach(viewModel.data) { item in
        
           ZStack {
                        
                     NavigationLink(destination: CallDetailView())
                     .{EmptyView()}.opacity(0.0)
                        
                     PhoneItemView(vm: log, callHandler: { _ in
                         print("swipe action tapped")

                          NavigationLink(destination: DetailView())                                       
                          { EmptyView() }
                     })
                        
            }
        }
}

But, even-though the log 'swipe action tapped' gets printed. DetailView is not loaded. Also, there's an warning on the NavigationLink of DetailView.

Result of 'NavigationLink<Label, Destination>' initializer is unused

Any help would be much appreciated!


Solution

  • A NavigationLink is a View that triggers a navigation action when tapped. It isn't just a Void closure that you can execute a demand. You need to add the NavigationLink to your View hierarchy, you can't create it inside a void closure.

    If you want to trigger navigation conditionally, you need to be using the NavigationLink initialiser which takes an isActive Binding instead and set the Binding to true when you want to trigger the navigation link.

    @State private var navigateToDetailView: Bool = false
    
    List {
      ForEach(viewModel.data) { item in
    
        ZStack {
    
          NavigationLink(destination: CallDetailView())
            .{EmptyView()}.opacity(0.0)
    
          PhoneItemView(vm: log, callHandler: { _ in
            print("swipe action tapped")
            // Set the binding to true to trigger navigation
            navigateToDetailView = true
          })
    
        }
      }
    }
    // Add the NavigationLink to your view
    .background(
      NavigationLink(
        destination: DetailView(),
        isActive: $navigateToDetailView
      )
      {
        EmptyView()
      }
    )
    

    If you want to be able to handle more than one destination, you just need to modify your @State to be an optional enum, representing your destination view. Your isActive Binding also has to change so that it is active in case your enum has a non-nil value.

    struct ViewWithMultipleConditionalDestinations: View {
      private enum Destination {
        case one
        case two
      }
    
      @State private var destination: Destination?
    
      var body: some View {
        NavigationView {
          VStack {
            Button("Destination one") {
              destination = .one
            }
    
            Button("Destination two") {
              destination = .two
            }
          }
          .background(
            NavigationLink(
              destination: destinationView,
              isActive: Binding(get: { destination != nil }, set: { if !$0 { destination = nil } }),
              label: { EmptyView() }
            )
          )
        }
      }
    
      @ViewBuilder
      private var destinationView: some View {
        switch destination {
        case .none:
          EmptyView()
        case .one:
          Text("One")
        case .two:
          Text("Two")
        }
      }
    }