16

I want to run account creation logic and then, if successful, transition to the destination view. Otherwise, I'll present an error sheet. NavigationLink transitions immediately to the destination view on tap.

I can get it to work if I create a phantom NavigationLink using the isActive overload and an empty string as the text (which creates a view with a zero frame). Then I toggle the isActive property with a Button presented to the user that runs the account creation logic first and at the end of the chain toggles the NavigationLink to active. I am inside a NavigationView.

        @State private var isActive: Bool = false

        NavigationView {

            // Name, Email, Password Textfields here

            // Button to run account creation logic:

            Button(action: {
                // Account creation promise chain here, then...
                self.isActive.toggle()
            }) {
                Text("Create Account")
            }

            // Phantom navigation link:

            NavigationLink("", destination: VerifyEmailView(email: email), isActive: self.$isActive)

        }

Is there a better way to do this? It seems bad practice to trigger running the account creation logic from a button, and then activate a phantom navigation link to transition to the next screen.

6
  • My answer (and the other answer in the same question) might be interesting: stackoverflow.com/a/57717462/1311272 Commented Aug 29, 2019 at 20:44
  • Thanks Sajjon, both answers are interesting. I'm trying to use the most SwiftUI-y way, so the answer by @kontiki was helpful, thank you for pointing me. Toughest parts for me with SwiftUI are 1) transitioning within the same NavigationView "stack" AFTER logic runs successfully (i.e. Create Account to Verify Email view), and also 2) transitioning to a new NavigationView "stack" (i.e. Successful login goes to a home view with its own NavigationView "stack"). Commented Aug 30, 2019 at 4:29
  • In general sounds like you are having a hard time thinking async? Looks like from your code that you think the NavigationLink for VerifyEmail will excute after account creation. That is NOT the case, that has to do with positioning of Views, not execution order. Commented Aug 30, 2019 at 6:05
  • Do you know how you would do this using UIKIT? Commented Aug 30, 2019 at 6:05
  • Yes, that's what I'm having a hard time with -- how to get the NavigationLink to activate only AFTER an account creation promise chain is complete. I built it in UIKit and I am trying to convert all the views and transitions to SwiftUI. Right now I use PromiseKit and in the last block push the new view controller onto the stack. I'm having a hard time with how to accomplish the same async work and then push a new controller only if successful. Commented Aug 30, 2019 at 16:05

4 Answers 4

16

There is a very simple approach to handle your views' states and NavigationLinks. You can notify your NavigationLink to execute itself by binding a tag to it. You can then set-unset the tag and take control of your NavigationLink.

struct SwiftView: View {
@State private var actionState: Int? = 0

var body: some View {

    NavigationView {
                VStack {
                    NavigationLink(destination: Text("Destination View"), tag: 1, selection: $actionState) {
                        EmptyView()
                    }


                    Text("Create Account")
                        .onTapGesture {
                            self.asyncTask()
                    }
                }
            }
    }

func asyncTask() {
    //some task which on completion will set the value of actionState
    self.actionState = 1
}

}

Here we have binded the actionState with our NavigationLink, hence whenever the value of actionState changes, it will be compared with the tag associated with our NavigationLink.

Your Destination View will not be visible until you set the value of actionState equal to the tag associated with our NavigationLink.

Like this you can create any number of NavigationLinks and can control them by changing just one Bindable property.

Thanks, hope this helps.

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

1 Comment

hint: using nil as the initial value for action state won't work. in my case, the navigation link was then still executed right away.
9

Building off of Mohit's answer, making a little more Swifty with an enum that encapsulates screen state:


enum ActionState: Int {
    case setup = 0
    case readyForPush = 1
}

struct SwiftView: View {
    @State private var actionState: ActionState? = .setup

    var body: some View {

        NavigationView {
                VStack {
                    NavigationLink(destination: SomeView, tag: .readyForPush, selection: $actionState) {
                        EmptyView()
                    }


                    Text("Create Account")
                        .onTapGesture {
                            self.asyncTask()
                    }
                }
            }
       }

func asyncTask() {
    //some task which on completion will set the value of actionState
    self.actionState = .readyForPush
}

Comments

4

You can wrap your Destination View in a lazy view to prevent the immediate invocation. Here's an example:

struct LazyView<Content: View>: View {
    let build: () -> Content
    init(_ build: @autoclosure @escaping () -> Content) {
        self.build = build
    }
    var body: Content {
        build()
    }
}

Eventually, invoke it like this:

NavigationLink(destination: LazyView(Text("Detail Screen"))){//your code}

I've written a full blog post covering this a few other pitfalls of NavigationLinks in SwiftUI. Refer here.

Comments

1

There is a new way to do that. Using NavigationStack and .navigationDestinations. Here is an example:

In the RegistrationScreen.swift file.

struct RegistrationScreen: View {
    var body: View {
        NavigationStack {
            VStack {
                Button {
                    viewModel.createAccount()
                } label: {
                    Text("Create an Account")
                }
            }
            .navigationDestination(isPresented: $viewModel.registerSuccess) {
                HomeScreen()
            }
            .alert("Server says", isPresented: $viewModel.registerFail) {
                Button("OK", role: .cancel) {}
            } message: {
                Text("Something went wrong while registering 😕")
            }
        }
    }
}

In the RegistrationViewModel.swift file.

class RegistrationViewModel: ObservableObject {
    @Publisher var isLoading: Bool = false
    @Publisher var registerSuccess: Bool = false
    @Publisher var registerFail: Bool = false

    func createAccount() {
        isLoading = true

        // Your code here

        if status.code == 201 {    // Means created
            registerSuccess = true
        } else {
            registerFail = true
        }
        isLoading = false
    }
}

In the HomeScreen.swift file.

struct HomeScreen: View {
    var body: View {
        Text("Please Give Like : 👍 ")
    }
}

1 Comment

Very clearly to understand. The only thing work for me after a day searching! Many thanks, bro.

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.