SwiftUI - `onAppear` and `onDisappear` action ordering

Hello,

I don't quite understand how to accomplish proper action ordering when triggering actions based on moving around in a NavigationView. I have code like this:

struct NavTestMaster: View {
  var body: some View {
    NavigationView {
      NavigationLink(destination: NavTestDetailView()) {
        Text("Hello world")
          .onAppear(perform: { print("Root is appearing") })
          .onDisappear(perform: { print("Root is disappearing") })
      }
    }
  }
}

struct NavTestDetailView: View {
  var body: some View {
    Text("I am a view into a view...")
      .onAppear(perform: { print("Detail view is appearing...") })
      .onDisappear(perform: { print("Detail view is disappearing...") })
  }
}

Actions are triggered when we click on the detail view, and then back, such that we have:

Root is appearing
Detail view is appearing...
Root is disappearing
Root is appearing
Detail view is disappearing...

But what I want is something like:

Root is appearing
Root is disappearing
Detail view is appearing...
Detail view is disappearing...
Root is appearing

How do I accomplish this?

Thanks for any thoughts!

PS - I apologize if this is off-topic. It isn't clear to me where in these forums we should discuss SwiftUI.

What's happening here is that NavigationLinks preload the destination views.
Current init -> onAppear -> destination init -> destination onAppear

I suppose it's a way to precache the layout of further pages in the navigation tree, but who knows.

One popular suggestion is to use a LazyView which is basically a view that takes a Content callback as an argument (like NavigationView for example) that is also an @autoclosure: this tells the framework to not execute it directly, and you will have the desired output.

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

Usage

struct NavTestMaster: View {
  var body: some View {
    NavigationView {
      NavigationLink(destination: LazyView(NavTestDetailView())) {
        Text("Hello world")
          .onAppear(perform: { print("Root is appearing") })
          .onDisappear(perform: { print("Root is disappearing") })
      }
    }
  }
}

Thanks a lot for taking the time to compose an answer to my question. I think I sort of understand the LazyView struct, but it isn't obvious to me why the compiler wouldn't squeeze everything down and, indeed, if I run this code:

struct NavTestMaster: View {
  var body: some View {
    NavigationView {
      NavigationLink(destination: LazyView(NavTestDetailView())) {
        Text("Mellow, whirled")
          .onAppear(perform: { print("Root is appearing") })
          .onDisappear(perform: { print("Root is disappearing") })
      }
    }
  }
}

struct NavTestDetailView: View {
  var body: some View {
    Text("I am a view into a view...")
      .onAppear(perform: { print("Detail view is appearing...") })
      .onDisappear(perform: { print("Detail view is disappearing...") })
  }
}

(with your LazyView struct copied into another file), I get the same order of function calls for appearing and disappearing I did before:

Root is appearing
Detail view is appearing...
Root is disappearing
Root is appearing
Detail view is disappearing...

I don't really care in this case if the detail view appears before root disappears, but the real code I'm working on would like to run the detail onDisappear code before the root onAppear code runs when navigating back.

Maybe another way to accomplish this would be to add code to the Back button somehow...

This is what I settled on. I don't know if this is best practices, and I'm sort of irritated I need to build my own "back" button, but this works:

struct NavTestMaster: View {
  var body: some View {
    NavigationView {
      NavigationLink(destination: NavTestDetailView()) {
        Text("Mellow, whirled")
          .onAppear(perform: { print("Root is appearing") })
          .onDisappear(perform: { print("Root is disappearing") })
      }
    }
  }
}

struct NavTestDetailView: View {
  @Environment(\.presentationMode) var mode: Binding<PresentationMode>

  var body: some View {
    VStack {
      Text("I am a view into a view...")
      Button(action: {
        print("Calling back button code...")
        self.mode.wrappedValue.dismiss()
      }) {
        Text("Go back!")
      }
    }
    .onAppear(perform: { print("Detail view is appearing...") })
    .onDisappear(perform: { print("Detail view is disappearing...") })
    .navigationBarBackButtonHidden(true)
  }
}

This produces the following in the log:

Root is appearing
Detail view is appearing...
Root is disappearing
Calling back button code...
Root is appearing
Detail view is disappearing...

and in my real code this approach is causing the "back button code" to run before the root calls onAppear.

1 Like

I've sometimes passed a closure down, like this. Another option for you...

struct NavTestMaster: View {
  var body: some View {
    NavigationView {
      NavigationLink(destination: NavTestDetailView(onDone: self.detailDone)) {
        Text("Hello world")
          .onAppear { print("Root is appearing") }
          .onDisappear { print("Root is disappearing") }
      }
    }
  }
  func detailDone() {
    print("Detail is done")
  }
}

struct NavTestDetailView: View {
  let onDone: () -> Void
  var body: some View {
    Text("I am a view into a view...")
      .onAppear { print("Detail view is appearing...") }
      .onDisappear { print("Detail view is disappearing..."); self.onDone() }
  }
}

Thanks for offering a suggestion. That is a cool idea and I should try it sometime. I don't think it will work in this case though because my problem was that the child view's onDisappear executes after the root view's onAppear when going back in the navigation hierarchy.

Still, a cool idea I'll be sure to use somewhere!

You said you wanted the different order, but I don't think you mentioned why. (Sorry if I missed it.) I imagined it was because you wanted to execute some code in the Root after the Detail had disappeared.

In this case, my detail view is interacting with my model, and when the user goes "back" to the root view, I'd like to capture some state and then present updated values in root. So I need that state capture to actually finish before the root is presented again. Just using onAppear and onDisappear wasn't achieving that.