0

I have custom Button Components that can trigger an action but I do want to navigate after the action is completed.

I tried putting the button component into the NavigationLink but it doesn't navigate after all and the button loses its touch animation.

The View:

struct LoginView: View {
    
    // initializers ---
    let K = Constants()

    // States ---
    @State private var email: String = ""
    @State private var isLoggedIn = false
    
    var body: some View {
            VStack(alignment: .leading) {
                
                // other UI components

                CustomTextField(
                    email: $email,
                    imageName: "envelope",
                    placeholder: "Enter e-mail address"
                )
                
                // other UI components
                
                FullWidthButton(
                    btnLable: "Continue with Apple",
                    onPressHandler: onAppleLogin,
                    backlgroundColor: Color(K.black),
                    prefixIconName: K.appleLogo
                )
                
                FullWidthButton(
                    btnLable: "Continue with Facebook",
                    onPressHandler: onFaceBookLogin,
                    backlgroundColor: Color(K.fbBlue),
                    prefixIconName: K.fbLogo
                )

                Spacer()
                
                // this implementation cause the loss of touch animation and didn't even navigate
                NavigationLink {
                    HomeView()
                } label: {
                    FullWidthButton(
                        btnLable: "Continue",
                        onPressHandler: onContinuePress,
                        backlgroundColor: Color(K.primary)
                    )
                }
                .simultaneousGesture(TapGesture().onEnded({ _ in
                    onContinuePress()
                }))

                
            }
    }
    
    // submit handler triggered when Continue btn pressed
    func onContinuePress() {
        print("aslkdfja")
    }

}

The button component

struct FullWidthButton: View {
    
    // initialisers ---
    let K = Constants()
    
    // porperties ---
    var btnLable: String
    var onPressHandler: () -> Void
    var backlgroundColor: Color
    var prefixIconName: String?
    
    var body: some View {
        Button {
            onPressHandler()
        } label: {
            ZStack{
                RoundedRectangle(cornerRadius: 27.0)
                    .fill(backlgroundColor)
                    .frame(height: 54)
                    .padding(.horizontal)
                    .overlay(alignment: .center) {
                        HStack(alignment: .center) {
                            if let imageName = prefixIconName {
                                Image(imageName)
                            }
                            Text(btnLable)
                                .font(.custom(K.poppins, size: 16))
                                .fontWeight(.medium)
                                .foregroundColor(.white)
                        }
                        
                    }
            }
        }
        .padding(.bottom)
    }
}

I want to call the function first and then navigate to the homeView() based on the result of the function. I've tried to put the FullWidthButton() into the NavigationLink(destinatin:label:) but it doesn't navigate to the View. I've searched on google and some posts showed me to use NavigationLink() with isActive: flag but it gives me warning of deprecated in ios 16.

5
  • NavigationLink is already a Button, there is no need to have another one inside it. Does this answer your question: stackoverflow.com/questions/60260503/… or this: stackoverflow.com/questions/57666620/… Commented Aug 24, 2023 at 6:00
  • @workingdogsupportUkraine I've tried the second link but as I mentioned in the question it doesn't navigate. Commented Aug 24, 2023 at 6:28
  • 1
    Just to be clear, your LoginView is in a NavigationView or NavigationStack correct? NavigationLink does not work without one of those. Commented Aug 24, 2023 at 8:04
  • Does this answer your question? Update environment object on listRow click before navigation Commented Aug 24, 2023 at 9:52
  • Yes, my LoginView is in the NavigationView. The NavigationView is wrapped around the ContentView form where the app starts. Commented Aug 24, 2023 at 11:32

1 Answer 1

0

First, you cannot have a Button in a NavigationLink when a Label is required. So, when you use a NavigationLink where a Label is required, you need to provide a label. Simple.

Unfortunately, when you go creative and provide a button, the compiler does not emit a warning or error, and if you tap it, no navigation happens.

Second, as @workingdog support Ukraine already pointed out correctly, a NavigationLink needs to be embedded in a NavigationStack or a NavigationSplitView.

So, a minimal example would look like this:

struct ContentView: View {
    var body: some View {
        NavigationStack {
            NavigationLink("Show Detail View") {
                Text("The Detail View")
            }
        }
    }
}

This little snippet contains all the functionality provided by Navigation Stack, Split View, master/detail, back-button and works on multiple platforms.

This approach has a catch, though: as mentioned, you need to provide a Label, but in your use case you want to have a button that triggers the navigation. Luckily, there's a solution, too:

One way to achieve this, without tricks is to use the "programmatic navigation link" form. That is, there are forms to create navigation links where to set the navigation stack programmatically. An example may make this clear:

What you need is a data structure representing "paths" on a navigation stack. This is in essence a sequence of items, say an Array, where each item represents a view on the stack. The actual type of this item is kept intentionally abstract, which enables a great deal of versatility. Keep on reading.

This "paths" variable is defined as a @State variable in a view whose body contains your NavigationStack. Again, a few lines of code demonstrate this:

struct ContentView2: View {
    typealias Item = String
    @State private var paths: [Item] = []
    
    var body: some View {
        NavigationStack(path: $paths) {
            ... 
        }
    }
}

As you can see, the NavigationStack receives a parameter, an Array of "Items". Note also, that this is a Binding and its value can (and will) be modified by the NavigationStack (for example, when tapping the "Back" button).

Each item represents a view pushed on the stack. Initially, the array is empty, means there is no view pushed onto the navigation stack.

Also, the type of the items is rather abstract. You can make it what suits your needs, and this is usually, defining the data you want to render.

Now, what you need to do is, controlling this array (with your button), and secondly, declare what the pushed view should look like:

struct ContentView2: View {
    typealias Item = String
    @State private var paths: [Item] = []
    
    var body: some View {
        NavigationStack(path: $paths) {
            Button("Show View") {
                self.paths = ["Other View"]
            }
            .navigationDestination(for: Item.self) { item in
                OtherView(item: item)
            }
        }
    }
}

So, here you have your button, and when pressed, it says, that the stack – which represents your pushed views – should contain one item.

So, this gets rendered by the NavigationStack. When the back button will be tapped, the NavigationStack removes the top item, which actually makes the array empty.

This brief example shows the principle way how you may accomplish your goal. I intentionally omitted much of your code just to demonstrate the essence. Your task is till to separate the business logic from the presentation logic and combine all required peaces.

Note also, that when your logic gets more complex, you may consider to compute the "paths" variable outside of a view – say, in a "Model". You have great control about when a view will navigate and what the other view will display. For example, if the action starts an asynchronous task and you want to navigate only after the task returns a result, it can be easily achieved.

Keep in mind though, that the semantic should be "navigation". If it's not "navigation", you may want to present the other view differently, for example as a sheet.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for the detailed explanation @CouchDeveloper. Correct me if I am wrong, but according to this implementation do I have to create a separate data type for each of the screens in my app? Right now I may have 25 screens. In React Native we can navigate simply by calling the Navigation.navigate(name, args) function how can we achieve this in swiftUI. I apologies if I am sounding dumb because I am learning iOS development to shift my career from React Native to iOS developer. If possible can you show me the implementation by creating a sample repo for it.
It makes absolute sense to use a different type for the "view state" for every "root" view, i.e. "screen" and also an associated "Model". You can realise a state management similar to the AppState concept of Reactive. Just keep in mind, that views are driven by data. Navigation is a more complex topic. SwiftUI provides tools for navigation and many other concepts – but does not suggest a certain architecture. SwiftUI is fantastic – easy and challenging at the same time. Read tutorials, keep going :)

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.