Mark Moeykens Specialists Course Onboarding SwiftUI View

How can I add navigation Links to the buttons to make them navigate to the next Onboarding View? I’m new to Swift UI, and I’ve tried several times but with no success; please help.

Olu Ojuroye

A NavigationLink does not need to be embedded in a Button. In fact they respond like a Button. See this sample code:

struct ContentView: View {

    @State private var words = ["Alpha", "Bravo", "Charlie", "Delta", "Echo", "Foxtrot", "Golf"]
    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVStack(alignment: .leading) {
                    ForEach(words, id: \.self) { word in
                        NavigationLink(destination: Text(word)) {
                            Text(word)
                        }
                    }
                }
            }
        }
        .padding(.horizontal)
    }
}

Hi Chris,
Thanks very much for your response.
These are the two screen and my code from where I am trying to navigate from screen one to the second screen. I’m trying to navigate with the System Image buttons.


Kind regards,

Olu Ojuroye

@Olu123

Since you have 3 Onboarding screens to step through you may want to consider having an Onboarding Container View which controls what screen is active from the moment that the onboarding process starts.

For example:

The following is from a sample project I just coded up to show the process while using your Button label code.

ContentView:

struct ContentView: View {
    @State private var isOnboarding = true
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
        .padding()
        .fullScreenCover(isPresented: $isOnboarding) {
            //  On Dismiss
        } content: {
            // The Onboarding sequence
            OnboardingContainerView(isOnboarding: $isOnboarding)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

OnboardingContainerView:

enum OnboardingStep: Int {
    case onboarding1 = 0
    case onboarding2 = 1
    case onboarding3 = 2
}

struct OnboardingContainerView: View {
    @Binding var isOnboarding: Bool
    @State private var currentStep: OnboardingStep = .onboarding1

    var body: some View {
        ZStack {
            Color("background")
                .ignoresSafeArea()

            switch currentStep {
            case .onboarding1:
                Onboarding1(currentStep: $currentStep)
            case .onboarding2:
                Onboarding2(currentStep: $currentStep)
            case .onboarding3:
                Onboarding3(currentStep: $currentStep, isOnboarding: $isOnboarding)
            }
        }
    }
}

struct OnboardingContainerView_Previews: PreviewProvider {
    static var previews: some View {
        OnboardingContainerView(isOnboarding: .constant(true))
    }
}

Onboarding1:

struct Onboarding1: View {
    @Binding var currentStep: OnboardingStep
    var body: some View {
        VStack(spacing: 40) {
            Text("Onboarding screen 1")
            Button {
                withAnimation {
                    currentStep = .onboarding2
                }
            } label: {
                Image(systemName: "chevron.right")
                    .foregroundColor(.white)
                    .padding(15)
                    .background(Circle())
            }
            .padding()
            .background(
                Circle()
                    .trim(from: 0, to: 0.33)
                    .stroke(Color.green, lineWidth: 4)
                    .rotationEffect(.degrees(-90))
            )

        }
        .accentColor(.green)
    }
}

struct Onboarding1_Previews: PreviewProvider {
    static var previews: some View {
        Onboarding1(currentStep: .constant(.onboarding1))
    }
}

Onboarding2:

struct Onboarding2: View {
    @Binding var currentStep: OnboardingStep
    var body: some View {
        VStack(spacing: 40) {
            Text("Onboarding screen 2")
            Button {
                withAnimation {
                    currentStep = .onboarding3
                }
            } label: {
                Image(systemName: "chevron.right")
                    .foregroundColor(.white)
                    .padding(15)
                    .background(Circle())
            }
            .padding()
            .background(
                Circle()
                    .trim(from: 0, to: 0.66)
                    .stroke(Color.green, lineWidth: 4)
                    .rotationEffect(.degrees(-90))
            )

        }
        .accentColor(.green)
    }
}

struct Onboarding2_Previews: PreviewProvider {
    static var previews: some View {
        Onboarding2(currentStep: .constant(.onboarding1))
    }
}

Onboarding3:

struct Onboarding3: View {
    @Binding var currentStep: OnboardingStep
    @Binding var isOnboarding: Bool
    var body: some View {
        VStack(spacing: 40) {
            Text("Onboarding screen 3")
            Button {
                withAnimation {
                    isOnboarding = false
                }
            } label: {
                Image(systemName: "chevron.right")
                    .foregroundColor(.white)
                    .padding(15)
                    .background(Circle())
            }
            .padding()
            .background(
                Circle()
                    .trim(from: 0, to: 1)
                    .stroke(Color.green, lineWidth: 4)
                    .rotationEffect(.degrees(-90))
            )

        }
        .accentColor(.green)
    }
}

struct Onboarding3_Previews: PreviewProvider {
    static var previews: some View {
        Onboarding3(currentStep: .constant(.onboarding1), isOnboarding: .constant(true))
    }
}

Hope that helps.

2 Likes

Thanks very much, Chris; this helped. I appreciate this.

Regards,

olu123

1 Like

Good day Chris. I implemented your suggestion, and it worked with the Onboarding screen". However, I am trying to link from the “Login Screen page” to the “Onboarding Screen Page” to Main Menu. Since I need to create separate Views for these, how do I link these?

Regards,

Olu123

1 Like

@Olu123

What is your intended flow when a user first runs the App or when an existing user Logs in?

For a new user, do you present an Onboarding flow that outlines the purposes of the App after which you present a Sign In screen?

…or do you present the onboarding screens to the user to describe the purposes of App and then store a Boolean in @AppStorage to record that the user has seen the Onboarding screens after which they never see those screens again and then present the Login/Sign-up screen?

Chris Ching has a “Chat App” tutorial on the CWC+ site that covers this kind of Onboarding scenario and the logic that controls it all. The login process in this case is a little different where it uses Phone Authentication (rather than email & password) where you key in your Phone number and then wait for a 6 digit code to be sent to your device and then key that code in as the Verification.

The OnBoarding process incorporates the phone login & verification process and then you go back to the HomeView after sign in has been completed.

Hi Chris,

Thanks again for your suggestion. My intention is that when a user downloads the App, it should start with the Onboarding Screens of three pages that introduce the App to the user and then store a Boolean in AppStorage to record their attendance and then take them to the Main Menu or Home page where they’ll begin their discovery further into the App.

Regards,

Olu123

Hi Chris,

The attached screen sequence is what I would like the Onboarding flow to be, followed by the Login Screen and then the Main Menu or Home Page.

Regards,

Olu123

@Olu123

Using the sample code I provided earlier, here is an updated version of ContentView which will hopefully give you an idea of how to go about it. @AppStorage saves (remembers) the state of those values and retrieves them when the App is next launched.

struct ContentView: View {
    @AppStorage("isOnboarding") var isOnboarding = true
    @AppStorage("userLoggedIn") var userLoggedIn = false
    @State private var showingLoginView = false

    var body: some View {
        NavigationStack {
            ZStack {
                Color("background")
                    .ignoresSafeArea()
                VStack(spacing: 40) {
                    Image(systemName: "globe")
                        .imageScale(.large)
                        .foregroundColor(.accentColor)
                    Text("Welcome")
                        .font(.title)

                    Button {
                        userLoggedIn = false
                    } label: {
                        Text("Log Out")
                    }

                }
                .onAppear {
                    if !isOnboarding && !userLoggedIn {
                        showingLoginView = true
                    }
                }
                .onChange(of: userLoggedIn) { newValue in
                    if newValue == false {
                        showingLoginView = true
                    }
                }
                .padding()
                .fullScreenCover(isPresented: $isOnboarding) {
                    // On dismiss of Onboarding
                    showingLoginView = true

                } content: {
                    // The Onboarding sequence
                    OnboardingContainerView(isOnboarding: $isOnboarding)
                }
                .fullScreenCover(isPresented: $showingLoginView) {
                    LoginView(userLoggedIn: $userLoggedIn, showingLoginView: $showingLoginView)
                }
            }
            .navigationTitle("Home View")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

I have added a LoginView just for the purposes of the exercise but the logic in that still applies even though it is just a skeleton View. The submit button simulates a successful login.

struct LoginView: View {
    @Binding var userLoggedIn: Bool
    @Binding var showingLoginView: Bool

    @State private var email = ""
    @State private var password = ""

    var body: some View {
        VStack {
            Text("Login")
                .font(.largeTitle)
                .bold()
                .padding(.top, 50)

            Spacer()

            TextField("Email", text: $email)
            SecureField("Password", text: $password)

            Spacer()

            Button("Submit") {
                userLoggedIn = true
                showingLoginView = false
            }
            .padding(.bottom, 50)
        }
        .textFieldStyle(.roundedBorder)
        .padding(.horizontal)
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView(userLoggedIn: .constant(false), showingLoginView: .constant(true))
    }
}

Hi Chris,

Thanks very much. I’ll work on this and get back to you on how far I’m able to make it work.

Regards,

Olu123

Hi Chris,
Please look at the error message I received from the “ContentView” after implementing the suggested corrections.

Regards,

Olu123

Hi Chris,

I discovered that after creating the “LoginView”, the App ignored the former “Login.” Do I need to replace that with the new “login view” I created or use them together?

Regards,

olu123

What version of Xcode are you using? You should be using the latest version which is Xcode 14.2.

Hi Chris,

I’m using Version 13.4.1 I’m very reluctant to update to version 14 because I won’t be able to open the work that I’ve been doing since Xcode usually give an error message that they’re in the past.

Regard,

olu123

Xcode 14.2 will open a project created in Xcode 13.4.1 since it is only 1 major revision earlier. It’s not like going from Xcode 7 to Xcode 14 where you will have major issues.

Okay, I’ll try. So, once I update to Xcode 14, the App will work?

olu123

The other alternative to downloading Xcode 14 is that you change NavigationStack to NavigationView and then add the modifier:

.navigationViewStyle(.stack)

to the trailing brace of the NavigationView. Like this:

struct ContentView: View {
    @AppStorage("isOnboarding") var isOnboarding = true
    @AppStorage("userLoggedIn") var userLoggedIn = false
    @State private var showingLoginView = false

    var body: some View {
        NavigationView {
            ZStack {
                Color("background")
                    .ignoresSafeArea()
                VStack(spacing: 40) {
                    Image(systemName: "globe")
                        .imageScale(.large)
                        .foregroundColor(.accentColor)
                    Text("Welcome")
                        .font(.title)

                    Button {
                        userLoggedIn = false
                    } label: {
                        Text("Log Out")
                    }

                }
                .onAppear {
                    if !isOnboarding && !userLoggedIn {
                        showingLoginView = true
                    }
                }
                .onChange(of: userLoggedIn) { newValue in
                    if newValue == false {
                        showingLoginView = true
                    }
                }
                .padding()
                .fullScreenCover(isPresented: $isOnboarding) {
                    // On dismiss of Onboarding
                    showingLoginView = true

                } content: {
                    // The Onboarding sequence
                    OnboardingContainerView(isOnboarding: $isOnboarding)
                }
                .fullScreenCover(isPresented: $showingLoginView) {
                    LoginView(userLoggedIn: $userLoggedIn, showingLoginView: $showingLoginView)
                }
            }
            .navigationTitle("Home View")
        }
        .navigationViewStyle(.stack)
    }
}

Eventually you are going to have to install Xcode 14 and get used to the new way that Navigation works in iOS.

Hi Chris,

Thanks very much. I’ve tried to download Xcode 14, but it keeps defaulting to the initial Xcode 13 from Apple Store. I think this is because It is still recognising that I have Xcode 13 on my computer. I have just deleted the Xcode 13 from my computer and started the download afresh. I’ll get you posted as I progress. Thank you very, very much. You’re amazing!

Olu123

@Olu123

What version of macOS do you have? That might restrict you to the version of Xcode that is available to you.