NEW: Learn to build amazing SwiftUI apps for macOS with my new book! >>

SOLVED: How to activate NavigationLink based on picker selection

Forums > SwiftUI

Hi

I have a picker displaying the contents of an array and a "special" selection at the top of the list. What I want is for the app to go to AddNewItemView() when the user selects the top item. I have been trying to do this using a NavigationLink:

struct MyPickerView: View {
    @State private var selectedItem: String = ""

    var itemList = [ "item1", "item2", "item3", "item4" ]

    var body: some View {
        NavigationView {
            Form {
                Section {
                    HStack {
                        Text("Item:")
                            .fontWeight(.bold)
                        Picker("Choose item", selection: $selectedItem) {

                            Text("Add New Item").tag("Add")

                            ForEach(itemList, id: \.self) { item in
                                Text(item).tag(item)
                            }
                        }
                        Spacer()
                    }
                    Spacer()

                    NavigationLink(
                        tag: "Add New Item",
                        selection: $selectedItem,
                        destination: AddNewItemView(),
                        label: { EmptyView() }
                    )
                }
            }
        }
    }
}

There are problems with the syntax of the NavigationLink which I have been unable to resolve:

  1. selection: needs to be an optional but making it an optional causes the selection not to be made
  2. Xcode has a type mismatch issue with AddNewItemView()

Can the syntax be sorted out, or, is there a better way to do what I want to do?

Cheers John

1      

I think there are a few problems with your use of NavigationLink(tag:, selection:, destination:, label:).

First, the tag you specify is what is used by SwiftUI to understand whether the link is active or not. You have hardcoded your tag to be "Add New Item". The Binding $selectedItem will never take that value because none of the tags used in your Picker correspond to that value. => Selection won't work as you would expect.

Second, the selection binding must be optional because SwiftUI uses the value of selectedItem to understand when the navigation link is active. If it is nil the navigation is inactive, means that your Form with the currrent Picker is shown. When the value of selectedItem corresponds to tag, the navigation (="perform segue"/"push destination view" in UIKit) is performed and your destination view is shown.

Third, your destination parameter must be passed as a closure, similar to how you specify the label:

                        destination: { AddNewItemView() },
                        label: { EmptyView() }

But in the end: I doubt that you want this form of NavigationLink. NavigationLink(tag:, selection:, destination:, label:) is better suited to Lists which have multiple rows with different tags and a list selection.

Probably NavigationLink(isActive:, destination:, label:) is better suited to your needs.

1      

Here is how I made your example work

  • I made the selectedItem a String? to make it compatible with NavigationLink(tag:, selection:...). Therefore I've added also as String? to all the tag modifiers in the Picker to make sure the types match (see here for a similar issue).
  • I've added an onDisappear on the Text("Add New Item").tag("Add" as String?) to trigger the Add selection again after 0.5 seconds. This is because the NavigationLink seems to clear the state of selectedItem when it re-appears on the screen and we can only trigger a new push navigation only after the previous back navigation (from the Picker selection) has finished.
  • I've put the NavigationLink in a background modifier of the Form to ensure it is invisible in the Form itself.
  • I made the itemList a state variable to allow adding entries from my AddItemView.
struct MyPickerView: View {
    @State private var selectedItem: String?

    @State private var itemList = [ "item1", "item2", "item3", "item4" ]

    var body: some View {
        NavigationView {
            Form {
                Section {
                    HStack {
                        Text("Item:")
                            .fontWeight(.bold)
                        Picker("Choose item", selection: $selectedItem) {

                            Text("Add New Item").tag("Add" as String?)
                                .onDisappear(perform: {
                                    print("Picker.Text.onDisappear")
                                    if selectedItem == "Add" {
                                        print("Triggering Add after 0.5 seconds")
                                        DispatchQueue.main.asyncAfter(deadline: .now()+0.5){
                                            print("Showing 'AddItemView'")
                                            selectedItem = "Add"
                                        }
                                    }
                                })

                            ForEach(itemList, id: \.self) { item in
                                Text(item).tag(item as String?)
                            }
                        }
                        Spacer()
                    }
                }
            }
            .background(
                NavigationLink(
                    tag: "Add",
                    selection: $selectedItem,
                    destination: {
                        AddItemView(add: { itemList.append($0); selectedItem = $0 })
                    },
                    label: {EmptyView()}
                )
            )
        }
    }
}

struct AddItemView: View {
    @Environment(\.dismiss) private var dismiss

    let add: (String) -> Void

    @State private var newItem: String = ""

    var body: some View {
        Form {
            Section {
                TextField("Item name", text: $newItem)
                    .navigationTitle("Add a new item")
            }

            Button("Add") {
                add(newItem)
                dismiss()
            }
        }
    }
}

1      

Hi @pd95

Thanks for your reply, suggestions and extra code.

My example was a simplified piece of code from a coredata app I am writing where the “item” has a name, description and other attributes.

I used your code and it works fine. I also experimented with using NavigationLink(isActive:, destination:, label:) and found that it also works and is simpler and more obvious code, so would be preferable.

Your solution of DispatchQueue with the 0.5s delay and using NavigationLink(isActive:...) both have the app returning to “MyPickerView” (displaying “Add New Item”) before bringing up the AddNewItem view. Unfortunately, although this is functional, from a user experience perspective in a production app, it would annoy me!

You also showed a solution for getting the new item back into the selectedItem variable which I knew needed to be solved and was going to be my next “project”. Thank you for that.

So I think I am back to the drawing boards to look for another way to present a user with a (picker) list and if they cannot find the item they want in that list, (somehow) go to an AddNewItem view.

Thanks again and I will mark this as solved. Cheers

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Spend less time managing in-app purchase infrastructure so you can focus on building your app. RevenueCat gives everything you need to easily implement, manage, and analyze in-app purchases and subscriptions without managing servers or writing backend code.

Get Started

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.