skip to Main Content

There is a pretty classic scenario when the view was shown and you send view shown event to some analytics platform.

How do I detect if the first view appeared after the second one was closed?

var body: some View {
    Button("Show second") {
        isSecondViewShown.toggle()
    }
    .onAppear {
        print("First appeared")
    }
    .fullScreenCover(isPresented: $isSecondViewShown) {
        Button("Close second") {
            isSecondViewShown.toggle()
        }
        .onAppear {
            print("Second appeared")
        }
    }
}

onAppear(which feels natural) does not work in this case. When you tap "Show Second" button and then "Close Second" button the following will be printed in logs:

First appeared
Second appeared

Instead of:

First appeared
Second appeared
First appeared

You can of course observe isSecondViewShown but it’s not reliable and misleading.

4

Answers


  1. Maybe you need to be more specific on your conditions, because as stated, while it’s hard to know when the first view is shown, it’s easy to know when the second view is closed (and the first view is then shown again). So the very simple thing to do is to watch when isSecondViewShown flips from true to false, meaning it’s no longer shown:

        @State var isSecondViewShown: Bool = false {
            didSet {
                if oldValue && !isSecondViewShown { // <-- watch for change from true to false
                    print("We are back to first view after showing the second view!")
                }
            }
        }
      
        var body: some View { // ... same as before
    

    Another option is a bit more involved: force onAppear on the view by having it in a container that has to redraw as a result of second view disappearance. This is by using an .id modifier:

        @State var isSecondViewShown: Bool = false
        @State var firstViewId: Int = 0 // <-- update it every time second view closes
      
        var body: some View {
            firstView
            .fullScreenCover(isPresented: $isSecondViewShown) {
                Button("Close second") {
                    isSecondViewShown.toggle()
                    firstViewId += 1 // <-- update the id to force redraw of the parent container of the view
                }
                .onAppear {
                    print("Second appeared")
                }
            }
        }
        
        var firstView: some View {
            ZStack { // <-- paren of the view we want the `onAppear` to be called
                Button("Show second (firstViewId)") { // <-- this view will call `onAppear` every time its parent was redrawn
                    isSecondViewShown.toggle()
                }
                .onAppear { // <-- this is what you want to happen every time second view disappears
                    print("First appeared")
                }
            }
            .id(firstViewId) // <-- this id will cause parent container to redraw
        }
    

    So the above will cause:

    Second appeared
    First appeared
    Second appeared
    First appeared
    

    As you can see I didn’t solve your actual issue: how to find out that the view is displayed (both solutions rely on second view disappearance instead). So these are workarounds really

    Login or Signup to reply.
  2. Instead of using .fullScreenCover(isPresented:) you can use .fullScreenCover(item:) by changing isSecondViewShown to an optional, you can use that. Here is the code I used:

    struct FirstScreen: View {
        @State private var selectedView: SecondScreen?
        var body: some View {
            NavigationStack {
                Button("Go to second screen") {
                    selectedView = SecondScreen()
                }
            }
            .onChange(of: selectedView){ _ in
                if selectedView == nil{
                    print("On first view.")
                }
            }
            .fullScreenCover(item: $selectedView) { screen in
                NavigationStack {
                    HStack {
                        screen
                    }
                    .toolbar {
                        HStack{
                            Button("Back") {
                                selectedView = nil
                            }
                        }
                    }
                }
            }
        }
    }
    

    And here is the SecondScreen struct:

    struct SecondScreen: View, Identifiable, Equatable{
        var id = UUID()
        
        var body: some View {
            Text("Second Screen")
                .onAppear{
                    print("On second screen")
                }
        }
    }
    

    And this usage can be modified and used in other ways like putting it in a array to make multi-pages.

    Login or Signup to reply.
  3. onAppear refers to the underlying UIView. Since you are using fullScreenCover the UIButton wasn’t removed, thus never disappeared, so the output is correct.

    Login or Signup to reply.
  4. The reason that the First appeared is not being printed the second time is because the .fullScreenCover acts like a sheet. Therefore, the first view has not actually disappeared yet.

    The first solution is through the use of onDisappear on the .fullScreenCover button. However, this means it’s not checking whether the first view is coming into view, but if the second one has fully left the view. Like so:

    struct ContentView: View {
    
        @State var isSecondViewShown: Bool = false
    
        var body: some View {
            Button("Show second") {
                isSecondViewShown.toggle()
            }
            .onAppear {
                print("First appeared")
            }
            .fullScreenCover(isPresented: $isSecondViewShown) {
                Button("Close second") {
                    isSecondViewShown.toggle()
                }
                .onAppear {
                    print("Second appeared")
                }
                .onDisappear {
                    print("First appeared")
                }
            }
        }
    }
    

    The second solution is a bit more work but allows you to have multiple pages working on the same view. But… there is no .fullScreenCover animation. This can be done like so:

    struct ContentView: View {
    
    @State var currentView: ViewTypes = .firstView
    
        var body: some View {
            if currentView == .firstView {
                Button("Show second") {
                    currentView = .secondView
                }
                .onAppear {
                    print("First appeared")
                }
            }
        
            else if currentView == .secondView {
                Button("Close second") {
                    currentView = .firstView
                }
                .onAppear {
                    print("Second appeared")
                }
            }
        }
    }
    

    Where ViewTypes is:

    enum ViewTypes {
        case firstView
        case secondView
    }
    

    Through the use of withAnimation however, you can achieve custom animations to fit your needs. Hope this helps!

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search