2

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.

wifizone
  • 41
  • 4
  • Might help: [How to check if a view is displayed on the screen?](https://stackoverflow.com/a/73038067/4667835) – Dávid Pásztor Mar 17 '23 at 14:22
  • Does this answer your question? [How to check if a view is displayed on the screen? (Swift 5 and SwiftUI)](https://stackoverflow.com/questions/60595900/how-to-check-if-a-view-is-displayed-on-the-screen-swift-5-and-swiftui) –  Mar 17 '23 at 16:51
  • @MrDeveloper No, these are also workarounds and not all of them apply to `fullScreenCover` – wifizone Mar 24 '23 at 11:20

4 Answers4

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

timbre timbre
  • 12,648
  • 10
  • 46
  • 77
1

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.

-1

onAppear refers to the underlying UIView. Since you are using fullScreenCover the UIButton wasn't removed, thus never disappeared, so the output is correct.

malhal
  • 26,330
  • 7
  • 115
  • 133
-1

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!

Chris
  • 92
  • 5