SwiftUser
SwiftUser

Reputation: 615

SwiftUI - Present onboarding screen/walkthrough

I wish to add/present an onboarding screen/walkthrough after user's have signed up. However, I'm already using .environmentObject() in my SceneDelegate. The reason being is I wish to check the Auth state of a user (I'm using FireStore).

So this is my SceneDelegate:

if let windowScene = scene as? UIWindowScene {
    let window = UIWindow(windowScene: windowScene)
    window.rootViewController = UIHostingController(rootView: InitialView().environmentObject(SessionStore()))
    self.window = window
    window.makeKeyAndVisible()
}

This is my InitialView:

struct InitialView: View {
    @EnvironmentObject var session: SessionStore
    func listen() {
        session.listenAuthenticationState()
    }
    var body: some View {
        Group {
            if session.isLoggedIn {
                MainView()
            } else {
                SignInView()
            }

        }.onAppear(perform: listen)
    }
}

My SignInView()is pretty self explanatory. And my MainView() is essentially my TabView

struct MainView: View {
    var body: some View {

        ZStack() {
    
            Color(SYSTEM_BACKGROUND_COLOUR)
            .edgesIgnoringSafeArea(.all)
            TabView{
                HomeView().tabItem({
                Image(systemName: "house.fill")
            })
            DiscoverSearchView().tabItem({
                Image(systemName: "magnifyingglass")
            })
                CameraView().tabItem({
                Image(systemName: "camera.fill")
            })

            ActivityView().tabItem({
                Image(systemName: "heart.fill")
            })
            ProfileView().tabItem({
                Image(systemName: "person.crop.circle")
            })
    

                }.accentColor(Color(SYSTEM_ACCENT_COLOUR))
        
    }
    }
}

So right now I have an OnboardingView, but I am not too sure how to present it:

struct OnboardingContentView: View {
    @State private var step = 1
    var body: some View {
        ZStack {
            Color(SYSTEM_BACKGROUND_COLOUR)
                .edgesIgnoringSafeArea(.all)
            VStack {
                Text("Welcome to").font(.caption).foregroundColor(Color(SYSTEM_FONT_COLOUR))
                    .padding(.top)
                Text("Anexis").bold().font(.largeTitle).foregroundColor(Color(SYSTEM_FONT_COLOUR))
                GeometryReader { gr in
                    HStack {
                        VStack(spacing: 40) {
                            Image("logo")
                            Text("1")
                                .padding()
                                .animation(Animation.interpolatingSpring(stiffness: 40, damping: 7).delay(0.1))
                        }.frame(width: gr.frame(in: .global).width)
                        VStack(spacing: 40) {
                            Image("logo")
                            Text("2")
                                .padding().fixedSize(horizontal: false, vertical: true)
                                .animation(Animation.interpolatingSpring(stiffness: 40, damping: 7).delay(0.1))
                        }.frame(width: gr.frame(in: .global).width)
                        VStack(spacing: 40) {
                            Image("logo")
                            Text("3")
                                .padding().fixedSize(horizontal: false, vertical: true)
                                .animation(Animation.interpolatingSpring(stiffness: 40, damping: 7).delay(0.1))
                        }.frame(width: gr.frame(in: .global).width)
                    }.multilineTextAlignment(.center)
                        .foregroundColor(Color(SYSTEM_FONT_COLOUR)).font(.title).padding(.vertical, 60).frame(width: gr.frame(in: .global).width * 3)
                        .frame(maxHeight: .infinity)
                        .offset(x: self.step == 1 ? gr.frame(in: .global).width : self.step == 2 ? 0 : -gr.frame(in: .global).width)
                        .animation(Animation.interpolatingSpring(stiffness: 40, damping: 8))
                }
                HStack(spacing: 20) {
                    Button(action: {self.step = 1}) {
                        Image(systemName: "1.circle")
                            .padding().scaleEffect(self.step == 1 ? 1 : 0.65)
                            
                        }
                    
                    Button(action: {self.step = 2}) {
                                     Image(systemName: "2.circle")
                                         .padding().scaleEffect(self.step == 2 ? 1 : 0.65)
                                         
                                     }
                    Button(action: {self.step = 3}) {
                                     Image(systemName: "3.circle")
                                         .padding().scaleEffect(self.step == 3 ? 1 : 0.65)
                                         
                        }
                }
                .animation(.spring(response: 0.4, dampingFraction: 0.5)).font(.largeTitle).accentColor(Color(SYSTEM_ACCENT_COLOUR))
                
                Button(action: {
                    NavigationView {
                        HomeView()
                    }
                }) {
                    HStack {
                        Text("Continue")
                        Image(systemName: "chevron.right")
                    }.padding(.horizontal)
                        .padding().background(Capsule().fill(Color(SYSTEM_ACCENT_COLOUR))).accentColor(Color(SYSTEM_BACKGROUND_COLOUR)).opacity(step == 3 ? 1 : 0)
                        .animation(.none).scaleEffect(step == 3 ? 1 : 0.01).animation(Animation.interpolatingSpring(stiffness: 50, damping: 10, initialVelocity: 10))
                }
            }.padding()
        }
    }
}

I've tried to add another .environmentObject() to my SceneDelegate but I get errors. So I thought I can use UserDefaults but I get an error

No ObservableObject of type ViewRouter found

So how could I go about checking if the user has installed the app for the first time, present the onboarding view and also how would I dismiss the onboarding view? Use a NavigationLink to my HomeView when the Continue button is clicked?

Edit :

class ViewRouter: ObservableObject {
    
    @Published var currentPage: String
    init() {
        if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
            UserDefaults.standard.set(true, forKey: "didLaunchBefore" )
            currentPage = "onboardingView"
        } else {
            currentPage = "homeView"
        }
    }
    
}

Upvotes: 1

Views: 908

Answers (1)

pawello2222
pawello2222

Reputation: 54466

You can create a top level view used for routing - displaying views conditionally:

class ViewRouter: ObservableObject {
    @Published var currentPage = "onboardingView"

    init() {
        if !UserDefaults.standard.bool(forKey: "didLaunchBefore") {
            UserDefaults.standard.set(true, forKey: "didLaunchBefore")
            currentPage = "onboardingView"
        } else {
            currentPage = "initialView"
        }
    }
}
struct RoutingView: View {
    @EnvironmentObject private var viewRouter: ViewRouter

    var body: some View {
        VStack {
            if viewRouter.currentPage == "onboardingView" {
                OnboardingView()
            } else {
                InitialView()
            }
        }
    }
}
struct OnboardingView: View {
    @EnvironmentObject private var viewRouter: ViewRouter
    var body: some View {
        VStack {
            Text("OnboardingView")
            Button("Continue") {
                self.viewRouter.currentPage = "initialView"
            }
        }
    }
}

You also need to replace the root view with:

RoutingView()
    .environmentObject(SessionStore())
    .environmentObject(ViewRouter())

Note: I assumed you want to present the InitialView and not the HomeView (as the user may not be logged in). If that's not true you can easily replace InitialView with HomeView.

Upvotes: 2

Related Questions