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.

  • `NavigationLink` is already a Button, there is no need to have another one inside it. Does this answer your question: https://stackoverflow.com/questions/60260503/swiftui-action-when-press-on-navigationlink or this: https://stackoverflow.com/questions/57666620/is-it-possible-for-a-navigationlink-to-perform-an-action-in-addition-to-navigati – workingdog support Ukraine Aug 24 '23 at 06:00
  • @workingdogsupportUkraine I've tried the second [link](https://stackoverflow.com/questions/57666620/is-it-possible-for-a-navigationlink-to-perform-an-action-in-addition-to-navigati) but as I mentioned in the question it doesn't navigate. – Rudra Soni Aug 24 '23 at 06:28
  • 1
    Just to be clear, your `LoginView` is **in** a `NavigationView` or `NavigationStack` correct? `NavigationLink` does not work without one of those. – workingdog support Ukraine Aug 24 '23 at 08:04
  • Does this answer your question? [Update environment object on listRow click before navigation](https://stackoverflow.com/questions/62374453/update-environment-object-on-listrow-click-before-navigation) – lorem ipsum Aug 24 '23 at 09:52
  • Yes, my ```LoginView``` is in the ```NavigationView```. The ```NavigationView``` is wrapped around the ```ContentView``` form where the app starts. – Rudra Soni Aug 24 '23 at 11:32

1 Answers1

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.

CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • 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. – Rudra Soni Aug 28 '23 at 06:59
  • 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 :) – CouchDeveloper Aug 28 '23 at 07:28
  • okay great! thanks for the help @CouchDeveloper – Rudra Soni Aug 28 '23 at 10:45