Is there a SwiftUI equivalent for viewWillDisappear(_:) or detect when a view is about to be removed?
Asked Answered
O

5

49

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)

Otiliaotina answered 15/1, 2020 at 5:40 Comment(0)
S
66

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))
    }
}
Shaquana answered 15/1, 2020 at 6:52 Comment(6)
onDisappear isn't always being called in SwiftUI but this was a good workaround compared to the others. – Astigmatic
onDisappear is still unreliable on iOS 13.5 😒 this is a great workaround. – Colson
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. – Sapowith
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. – Sapowith
This is not working any way it's a good workaround. – Propeller
very interesting solution, it seems to work for me. But what is the purpose of having typealias UIViewControllerType = UIViewController? It isn't directly used, and in fact after removing it the code appears to work the same. Thanks in advance. – Fracture
C
3

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))
    }
}
Capriole answered 17/6, 2022 at 10:40 Comment(2)
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. – Dray
Any idea how to execute some code when the dismiss gesture (sliding back) is cancelled? – Cirilo
F
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
      }
    }
  }
}
Fluoroscopy answered 25/5, 2021 at 22:32 Comment(0)
G
-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()
        }
    }
}


Goner answered 22/7, 2021 at 14:26 Comment(2)
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. – Ras
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. – Ras
E
-5

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("?")
           }
        
        }

    }
}
Enclose answered 23/6, 2021 at 16:6 Comment(2)
Please explain briefly what this code does. Read How to Answer. – Engstrom
scenePhase, I believe, doesn't call onChange when changing between views in your app. scenePhase is for app backgrounding. – Ras

© 2022 - 2024 β€” McMap. All rights reserved.