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:
In the class
UserAuth
, create a staticshared
instance, that can be called anywhere in your code and ensures you are always using the same instance.Create a modifier extending
View
, that reads the status inUserAuth.shared
and shows the necessary view according to whether the authentication is required (LoginView()
if the user is not authenticated).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")
}
}
}
}