Home > Back-end >  SwiftUI private route based on Authentication Status
SwiftUI private route based on Authentication Status

Time:02-27

I need to implement a mechanism just like react private and public route on swiftUI. Basically I have tens of views and some of these views requires authentication based on user logged in status. So far I have tried to hold current screen in an Environment Object as show in following class

    enum Routes {
    case screenA,
         screenB,
         screenC,
         screenD,
         screenE,
         screenF,
         screenG,
         loginScreen
    
    var isAuthRequired: Bool {
            if case . screenA = self {
                return true
            } else if case . screenD = self {
                return true
            } else {
                return false
            }
        }
}

class AuthenticatedRoute: ObservableObject {
    
    @Published var currentRoute: Routes
    
    init(){
        self.currentRoute = . screenA
    }
}

And on my main screen I check every time the current screen change whether user loggedin and current page require authentication.

struct MainView: View {
    @StateObject var authenticatedRoute = AuthenticatedRoute()
    @EnvironmentObject var userAuth: UserAuth
    
    var body: some View {
        mainView()
            .environmentObject(authenticatedRoute)
    }
    
    
    @ViewBuilder
    func mainView() -> some View {
        if (self.authenticatedRoute.currentRoute.isAuthRequired && !userAuth.isLoggedIn) {
            LoginView()
        }
        else {
            DefaultTabView()
        }
    }
}

And this is an example of how I keep changing this environment variable. I change the environment object onAppear event method of view.

struct ScreenA: View {
    @EnvironmentObject var authenticatedRoute: AuthenticatedRoute
    
    var body: some View {
        NavigationView {
            someContent()
        }.onAppear {
            authenticatedRoute.currentRoute = .screenA
        }
    }
}

While this approach works for most cases for some reason it behave strange when a screen is in tab navigation. Also I do not feel comfortable with this solution, that I need to change screen name manually on every single page, and checking authentication status in main view. I think it would be better somehow if I can write a kind of interceptor before every page change and check if desired destination requires authentication and if user is authenticated but I could not find a way manage this. I'm relatively new to iOS development and had experience with react native but this should not be so hard to implement in my opinion since this is a requirement for most applications.

So basically I need to implement a private and public router in swiftUI or intercept every page change so I should not modify environment variable on each pages manually and should not check conditions in MainView inside a function.

CodePudding user response:

I can propose another approach, that does not require the class AuthenticatedRoute, I hope it's what you are looking for. The process is:

  1. In the class UserAuth, create a static shared instance, that can be called anywhere in your code and ensures you are always using the same instance.

  2. Create a modifier extending View, that reads the status in UserAuth.shared and shows the necessary view according to whether the authentication is required (LoginView() if the user is not authenticated).

  3. Use the modifier at the outermost container (VStack, NavigationView, whatever) of any view that requires the user to be authenticated.

The example below shows how this can work, if you want to run it:

1. Static UserAuth instance

class UserAuth: ObservableObject {
    static let shared = UserAuth()     // This is to assure that you refer to the same instance all over the code
    
    @Published private(set) var isLoggedIn = false    // Use the variable you already have
    
    func logInOrOff() {     // Implement each func as needed
        isLoggedIn.toggle()
    }
}

2. Create the View modifier

extension View {
    
    @ViewBuilder
    func requiresAuthentication() -> some View {
        if UserAuth.shared.isLoggedIn {  // "shared" is the same instance used by the views
            self
        } else {
            LoginView()
        }
    }
}

3. Apply the modifier at the bottom of the view that requires authentication

struct Example: View {
    
    @StateObject private var userAuth = UserAuth.shared    //  Or @EnvironmentObject, as you wish
    
    var body: some View {
        VStack {
            Text(userAuth.isLoggedIn ? "Now we're good" : "You must log in")
                .padding()
            
            Button {
                userAuth.logInOrOff()
            } label: {
                Text("Logoff")
            }
        }
        .requiresAuthentication()    // This is what makes your view safe
    }
}

struct LoginView: View {
    var body: some View {
        VStack {
            Text("Authentication is required")
                .padding()
            
            Button {
                UserAuth.shared.logInOrOff()
            } label: {
                Text("Log in")
            }
        }
    }
}
  • Related