Issue #825

For many apps that require user authentication, a common practice is to define a shared UserManager with an optional User. This is convenient but it requires us to constantly unwrap and check that user

class UserManager {
    static let shared = UserManager()
    private(set) var user: User?
}

A more safer approach is to leverage Swift type system and separate the need based on authenticated and unauthenticated usage. For example, for an unauthorized users, we show the login screen. For authorized users but haven’t completed profile customization yet, we show onboarding. And finally, show the main screen.

Define UserState

Depend on the app, we can define a UserState. Here, after the user has logged in with Apple ID, we need to onboard them with few questions for profile settings before taking them to the main flow.

enum UserState {
    case none
    case authorized(User)
    case registered(User, Profile)
}

Entry AppView

Then in our entry AppView, we can show different parts of the app depending on this userState

final class AppViewModel: ObservableObject {
    @Published var userState: UserState = .none
}

struct AppView: View {
    @StateObject var ViewModel = AppViewModel()

    var body: some View {
        content
            .onAppear {
                authService.checkUserState()
            }
    }

    @ViewBuilder
    private var content: some View {
        switch viewModel.userState {
        case .none:
            LoginView()
        case let .authorized(user):
            OnboardView(user: user)
        case let .registered(user, profile):
            MainView(user: user, profile: profile)
        }
    }
}

Onboard flow

We only show OnboardView given that the user has authorized, this requirement needs the user to be nonnil. Since our UserState is predictable we can just pass the user to the Onboard flow

Note here how we use underscore _ to access the container StateObject to initialize

final class OnboardViewModel: ObservableObject {
    private let user: User

    init(user: User) {
        self.user = user
    }
}

struct OnboardView: View {
    @StateObject private var viewModel: OnboardViewModel

    init(user: User) {
        self._viewModel = StateObject(wrappedValue: OnboardViewModel(user: user))
    }
    
    var body: some View {}
}

Read more