35

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,095
  • 24
  • 42

4 Answers4

50

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))
    }
}

backup

Asperi
  • 173,274
  • 14
  • 284
  • 455
0

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,163
  • 11
  • 21
0

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
  • 3,895
  • 1
  • 27
  • 45
  • 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. – ns. 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. – ns. May 27 '22 at 21:57
-3

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