45

In SwiftUI, I'm trying to find a way to detect that a view is about to be removed only when using the default navigationBackButton. Then perform some action.

Using onDisappear(perform:) acts like viewDidDisappear(_:), and the action performs after another view appears.

Or, I was thinking the above problem might be solved by detecting when the default navigationBarBackButton is pressed. But I've found no way to detect that.

Is there any solution to perform some action before another view appears?

(I already know it is possible to do that by creating a custom navigation back button to dismiss a view)

FRIDDAY
  • 3,781
  • 1
  • 29
  • 43

5 Answers5

64

Here is approach that works for me, it is not pure-SwiftUI but I assume worth posting

Usage:

   SomeView()
   .onDisappear {
        print("x Default disappear")
    }
   .onWillDisappear { // << order does NOT matter
        print(">>> going to disappear")
    }

Code:

struct WillDisappearHandler: UIViewControllerRepresentable {
    func makeCoordinator() -> WillDisappearHandler.Coordinator {
        Coordinator(onWillDisappear: onWillDisappear)
    }

    let onWillDisappear: () -> Void

    func makeUIViewController(context: UIViewControllerRepresentableContext<WillDisappearHandler>) -> UIViewController {
        context.coordinator
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<WillDisappearHandler>) {
    }

    typealias UIViewControllerType = UIViewController

    class Coordinator: UIViewController {
        let onWillDisappear: () -> Void

        init(onWillDisappear: @escaping () -> Void) {
            self.onWillDisappear = onWillDisappear
            super.init(nibName: nil, bundle: nil)
        }

        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            onWillDisappear()
        }
    }
}

struct WillDisappearModifier: ViewModifier {
    let callback: () -> Void

    func body(content: Content) -> some View {
        content
            .background(WillDisappearHandler(onWillDisappear: callback))
    }
}

extension View {
    func onWillDisappear(_ perform: @escaping () -> Void) -> some View {
        self.modifier(WillDisappearModifier(callback: perform))
    }
}
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • 1
    onDisappear isn't always being called in SwiftUI but this was a good workaround compared to the others. – Daniel Ryan Jun 02 '20 at 04:26
  • 1
    onDisappear is still unreliable on iOS 13.5 this is a great workaround. – Zorayr Jun 27 '20 at 04:17
  • This is a great idea. However, it isn't working for me on iOS 15. I'm getting `onWillDisappear` after `onDisappear`. However, I modified the code to invoke the callback in `dismantleUIViewController` instead of `viewWillDisappear` and I get the correct timing. – Timothy Moose Oct 08 '21 at 19:09
  • 1
    Actually, it doesn't work for me. The order is correct, but it seems to be too late in the view's lifecycle to perform animation. – Timothy Moose Oct 08 '21 at 19:51
  • This is not working any way it's a good workaround. – Kasun Udara Jayasekara Jul 13 '23 at 04:01
2

You can bind the visibility of the child view to some state, and monitor that state for changes.

When the child view is pushed, the onChange block is called with show == true. When the child view is popped, the same block is called with show == false:

struct ParentView: View {
  @State childViewShown: Bool = false

  var body: some View {
    NavigationLink(destination: Text("child view"),
                   isActive: self.$childViewShown) {
      Text("show child view")
    }
    .onChange(of: self.childViewShown) { show in
      if show {
        // child view is appearing
      } else {
        // child view is disappearing
      }
    }
  }
}
Matthew
  • 1,363
  • 11
  • 21
2

Here's a slightly more succinct version of the accepted answer:

private struct WillDisappearHandler: UIViewControllerRepresentable {

    let onWillDisappear: () -> Void

    func makeUIViewController(context: Context) -> UIViewController {
        ViewWillDisappearViewController(onWillDisappear: onWillDisappear)
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}

    private class ViewWillDisappearViewController: UIViewController {
        let onWillDisappear: () -> Void

        init(onWillDisappear: @escaping () -> Void) {
            self.onWillDisappear = onWillDisappear
            super.init(nibName: nil, bundle: nil)
        }

        @available(*, unavailable)
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }

        override func viewWillDisappear(_ animated: Bool) {
            super.viewWillDisappear(animated)
            onWillDisappear()
        }
    }
}

extension View {
    func onWillDisappear(_ perform: @escaping () -> Void) -> some View {
        background(WillDisappearHandler(onWillDisappear: perform))
    }
}
Gerry Shaw
  • 9,178
  • 5
  • 41
  • 45
Jonathan.
  • 53,997
  • 54
  • 186
  • 290
  • 1
    Note that onWillDisappear will get called if the user uses the slide back gesture to navigate back. The callback gets called even if the user doesn't complete the gesture. – Gerry Shaw Sep 24 '22 at 21:37
  • Any idea how to execute some code when the dismiss gesture (sliding back) is cancelled? – Robert Basamac Oct 07 '22 at 16:15
-1

you have a couple of actions for each object that you want to show on the screen

func onDisappear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view disappears.
func onAppear(perform action: (() -> Void)? = nil) -> some View
//Adds an action to perform when this view appears.

you can use the like the sample ( in this sample it affects on the VStack):


import SwiftUI

struct TestView: View {
    @State var textObject: String
    var body: some View {
                VStack {
                 Text(textObject)
               }
            .onAppear {
                textObject = "Vertical stack is appeared"
            }
            .onDisappear {
                textObject = ""
            }
    }
}

struct TestView_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            TestView()
        }
    }
}


mohsen
  • 4,698
  • 1
  • 33
  • 54
  • OP's question said: `Using onDisappear(perform:) acts like viewDidDisappear(_:), and the action performs after another view appears.` So `onDisappear` unfortunately won't solve the issue in the question. Hopefully newer versions of SwiftUI will have a straightforward way to solve this. – nishanthshanmugham May 27 '22 at 21:40
  • You can test this behaviour won't work for OP's question, by adding `print("...")` statements inside `onAppear`/`onDisappear` in the parent and child views. This is true at least in Xcode 13.4/iOS 15.5. – nishanthshanmugham May 27 '22 at 21:57
-4

You can trigger the change of the @Environment .scenePhase like this :

struct YourView: View {

    @Environment(\.scenePhase) var scenePhase

    var body: Some View {
        VStack {
           // Your View code
        }
        .onChange(of: scenePhase) { phase in
           switch phase {
            case .active:
                print("active")
            case .inactive:
                print("inactive")
            case .background:
                print("background")
            @unknown default:
                print("?")
           }
        
        }

    }
}
Onirix
  • 1
  • 1