0

I'm trying an authenticated flow in my app.

The app has a LoginScreenView that is going to appear first on the app. When logging in, we go to a TabView consisting of two Views, FirstView and SecondView. SecondView has a logout button, that returns the user to the login screen. On the FirstView we have a network call that is being performed always onAppear.

When the user logs out, we are on the SecondView, on the second tab; however both onAppear and onDisappear functions of the FirstView are being executed, even though FirstView is not on display.

This provokes in our use case to trigger the network call in the onAppear method of FirstView that will fail because we are not authenticated.

Here is my code.

This is LoginScreen:

struct LoginScreenView: View {
    @EnvironmentObject private var authManager: AuthManager
    
    var body: some View {
        Button("Login") {
            authManager.isLoggedIn = true
        }
    }
}

This is FirstView:

struct FirstView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
        .tabItem {
            Label("Hello", systemImage: "globe")
        }
        .onAppear {
            print("First ✅")
            Task {
                try await loadSomeStuffFromNetwork()
            }
        }
        .onDisappear {
            print("First ❌")
        }
    }
}

This is SecondView:

struct SecondView: View {
    @EnvironmentObject private var authManager: AuthManager
    
    var body: some View {
        VStack {
            Image(systemName: "flag.fill")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Goodbye, world!")
            Button("Logout") {
                authManager.isLoggedIn = false
            }
        }
        .tabItem {
            Label("Goodbye", systemImage: "flag")
        }
        .onAppear {
            print("Second ✅")
        }
        .onDisappear {
            print("Second ❌")
        }
    }
}

And this is the root view of the app:

struct ContentView: View {
    @StateObject private var authManager = AuthManager()
    
    var body: some View {
        TabView {
            Group {
                if isLoggedIn {
                    FirstView()
                    SecondView()
                } else {
                    LoginScreenView()
                }
            }
        }
        .environmentObject(authManager)
    }
}

It's using the AuthManager declared in here:

final class AuthManager: ObservableObject {
    @Published var isLoggedIn: Bool = false
}

As you can see in the code, we are trying to change the displayed views depending on whether we are logged in or not, and we use an if-else for that.

Do you think this code is OK? Do you feel that this might be a bug on the Apple side? I feel like this should work out of the spot, but I don't know if I'm missing anything.

I saw this question from 2022, but it seems it never got resolved (apart from the workaround of adding a condition of being logged in to perform the network call).

Also, saw this other one, where they propose to use a @State variable to make the view only perform one network call instead of everytime it appears (or SwiftUI decides it to appear).

Are we OK on this train of thought? Should we try to build our network call in some other way?

1 Answers1

0

TabView SwiftUI has a bug and it makes each tab render many times. To fix it you can use LazyView.

struct LazyView<Content: View>: View {
 let build: () -> Content
 init(_ build: @autoclosure @escaping () -> Content) {
    self.build = build
 }

 var body: Content {
    build()
 }
}

Use:

func tabViewRegion() -> some View {
    TabView(selection: $viewModel.tabViewCurrentIndex) {
        LazyView(YourView1())
            .tag(0)
        LazyView(YourView2())
            .tag(1)
    }
}

I hope Apple will fix it soon as soon.

Tài Lê
  • 63
  • 1
  • 10