Search code examples
swiftuser-interfaceswiftuitransitiongeometryreader

GeometryReader transitions between views cause onAppear trigger initially


I've been working on a project in SwiftUI 1.0 because it requires support for iOS 13.0 devices (So no fullScreenCover available). I require my application to have clean transition animations between my views, so I'm using GeometryReader extensively throughout my app, as follows:

recording

Right now I've completed 80% of my UI and I'm slightly trying to integrate the data fetching part. So, what I did was to call the data fetching methods in my onAppear method on my views. Here is my code:

struct ContentView: View {

    @State private var isUserLoggedIn = false

    var body: some View {
        GeometryReader { geometry in
            HomeView()
            LoginView(isUserLoggedIn: $isUserLoggedIn)
                .offset(y: isUserLoggedIn ? geometry.size.height + geometry.safeAreaInsets.bottom : 0)
        }
    }
}

struct LoginView: View {

    @Binding var isUserLoggedIn: Bool

    var body: some View {
        Button("Login") {
            withAnimation {
                isUserLoggedIn.toggle()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.red)
        .edgesIgnoringSafeArea(.all)
        .onAppear {
            print("Login appeared!")
        }
    }
}

struct HomeView: View {

    @State private var isTwoSelected = false

    var body: some View {
        GeometryReader { geometry in
            OneView(isTwoSelected: $isTwoSelected)
                .offset(x: isTwoSelected ? -geometry.size.width : 0)
            TwoView(isTwoSelected: $isTwoSelected)
                .offset(x: isTwoSelected ? 0 : geometry.size.width)
        }
    }
}

struct OneView: View {

    @Binding var isTwoSelected: Bool

    var body: some View {
        NavigationView {
            Button("Goto Two") {
                withAnimation {
                    isTwoSelected.toggle()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.green)
            .edgesIgnoringSafeArea(.all)
            .navigationBarTitle(Text("One"))
            .onAppear {
                print("One appeared! Fetch data...")
            }
        }
    }
}

struct TwoView: View {

    @Binding var isTwoSelected: Bool

    var body: some View {
        NavigationView {
            Button("Goto One") {
                withAnimation {
                    isTwoSelected.toggle()
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .background(Color.yellow)
            .edgesIgnoringSafeArea(.all)
            .navigationBarTitle(Text("Two"))
            .onAppear {
                print("Two appeared! Fetch data...")
            }
        }
    }
}

But the issue that I face now is that they are triggered all at once. You can see that the console prints as follows:

Login appeared!
Two appeared! Fetch data...
One appeared! Fetch data...

How would I go about fetching the data to the corresponding views only while they appear in the view frame of the device?


Solution

  • As stated by @Asperi in the comments, transition seems to be the right way to proceed here instead of offset. Here's the implementation with transition (only adding views that have been changed):

    struct ContentView: View {
    
        @State private var isUserLoggedIn = false
    
        var body: some View {
            if isUserLoggedIn {
                HomeView()
                    .transition(.asymmetric(insertion: .move(edge: .top), removal: .move(edge: .bottom)))
            } else {
                LoginView(isUserLoggedIn: $isUserLoggedIn)
                    .transition(.asymmetric(insertion: .move(edge: .top), removal: .move(edge: .bottom)))
            }
        }
    }
    
    struct HomeView: View {
    
        @State private var isTwoSelected = false
    
        var body: some View {
            Group {
                if !isTwoSelected {
                    OneView(isTwoSelected: $isTwoSelected)
                        .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
                } else {
                    TwoView(isTwoSelected: $isTwoSelected)
                        .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .leading)))
                }
            }
        }
    }