iOS 14 .onAppear() is called on DISappear instead of appear

I've got a View with
Code Block
.onAppear(perform: {
print("appear")
})
.onDisappear(perform: {
print("disappear")
})
set on it.

In iOS 13.5, when I navigate away from the view via a NavigationLink, it prints "disapear"; when I close the new view and get back to the first view "appear" gets printed. All as it should be.

Now in iOS 14, this happens instead:
  • when the view is first shown, it prints "appear"

  • when navigating away from the view, it prints "disappear" and "appear"

  • when navigating back to the view, nothing gets printed

  • when navigating away from the view, then "appear" gets printed, along with the expected "disappear"

...is this some new super-lazy way of doing things or just plain a bug?

It runs havoc with my login, as I set @Binding vars in the second view, which are evaluated back in the first view when .onAppear() runs. Worked great until iOS 14...
Was using relamquad's answer quite successfully until I noticed it can lead to memory leaks due to strong reference cycles.

Using it like described works fine:
Code Block Swift
var body: some View {
SomeView().uiKitOnAppear {
print("I am uiKitAppear")
}
}


However if you pass a ViewModel's function for example, the ViewModel will not be deallocated once the View should be deinitialized:
Code Block Swift
@ObservedObject var viewModel: ViewModel
var body: some View {
SomeView().uiKitOnAppear {
viewModel.startUpdating()
}
}


If been trying to find a solution using weak references, but then again, somehow startUpdating is not called on .uiKitOnAppear().
Does anybody know a solution to this?
Code Block
import SwiftUI
struct UIKitAppear: UIViewControllerRepresentable {
    let action: () -> Void
    func makeUIViewController(context: Context) -> UIAppearViewController {
       let vc = UIAppearViewController()
        vc.delegate = context.coordinator
        return vc
    }
    func updateUIViewController(_ controller: UIAppearViewController, context: Context) {}
    func makeCoordinator() -> Coordinator {
        Coordinator(action: self.action)
    }
    class Coordinator: ActionRepresentable {
        var action: () -> Void
        init(action: @escaping () -> Void) {
            self.action = action
        }
        func remoteAction() {
            action()
        }
    }
}
protocol ActionRepresentable: AnyObject {
    func remoteAction()
}
class UIAppearViewController: UIViewController {
    weak var delegate: ActionRepresentable?
    override func viewDidLoad() {
        view.addSubview(UILabel())
    }
    override func viewDidAppear(_ animated: Bool) {
        delegate?.remoteAction()
    }
}
public extension View {
    func onUIKitAppear(_ perform: @escaping () -> Void) -> some View {
        self.background(UIKitAppear(action: perform))
    }
}

Thanks gfdgdfg for the quick response and solution proposition.

In my specific case, it did not solve the memory leak issue. What helped was resetting the action in updateUIViewController(_ controller: UIAppearViewController, context: Context).

Extending relamquad's answer like this:
Code Block swift
struct UIKitAppear: UIViewControllerRepresentable {
    let action: () -> Void
    func makeUIViewController(context: Context) -> UIAppearViewController {
       let vc = UIAppearViewController()
        vc.action = action
        return vc
    }
    func updateUIViewController(_ controller: UIAppearViewController, context: Context) {
controller.action = action
}
}


I found that the issue with .onAppear exist only when I using ObservedObject in my View if I change it to the StateObject the problem is gone but I need to support an iOS 13 so as the temporary solution I calling my API call inside init of my view
Can someone please fix this? It's just so dumb! .onAppear() is very very wonky. Did you guys not even review the PRs before pushing out the commits? And the worst thing is .onDisappear() doesn't work at all.
Hi I am following the iOS App Dev Tutorials and I found this bug using Xcode 12.5.

Code Block
.onAppear {
            scrumTimer.reset(lengthInMinutes: scrum.lengthInMinutes, attendees: scrum.attendees)
            scrumTimer.startScrum()
            scrumTimer.speakerChangedAction = {
                player.seek(to: .zero)
                player.play()
            }
        }
        .onDisappear {
            scrumTimer.stopScrum()
            let newHistory = History(attendees: scrum.attendees, lengthInMinutes: scrumTimer.secondsElapsed / 60)
            scrum.history.insert(newHistory, at: 0)


The results is that when the view is shown, the onAppear is executed, then the onDisappear is executed and then again the onAppear. The results is that I have in the history 2 copies of the same entry.


I just updated to iOS 14.6 and it still doesn't work :(

Just started working through the SwiftUI tutorial for iOS on the apple developer website. Freshly installed xCode on a new M1 mac... onAppear AND onDisappear would trigger on appearance, and both would trigger AGAIN on disappearance.

Opened up the console and saw this assertion error

displayModeButtonItem is internally managed and not exposed for DoubleColumn style

People had similar issues with that error, adding this to the root NavigationView fixed the error for my use case

.navigationViewStyle(StackNavigationViewStyle())

Hope this helps someone!

onAppear and onDisappear don't work. However, this customized function did work!

extension View {
  func onDidAppear(_ perform: @escaping (() -> Void)) -> some View {
    self.modifier(ViewDidAppearModifier(callback: perform))
  }
}

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

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

struct ViewDidAppearHandler: UIViewControllerRepresentable {
  func makeCoordinator() -> ViewDidAppearHandler.Coordinator {
    Coordinator(onDidAppear: onDidAppear)
  }

  let onDidAppear: () -> Void

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

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

  typealias UIViewControllerType = UIViewController

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

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

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

    override func viewDidAppear(_ animated: Bool) {
      super.viewDidAppear(animated)
      onDidAppear()
    }
  }
}

I have the same issue like the OP, and it still exists in iOS 16 beta 2, whether I use NavigationSplitView or NavigationView with column-style. Are there no other view modifiers that are called? I have a singleton audio engine in the app, and the detail view initializes itself on the engine at .onAppear. Can't have a new audio engine for each detail view, especially if the previous detail view is not discarded.

SOLUTION:

Used .task instead of .onAppear. .task runs whenever the view appears so it works the same without bugging out. .onDisappear still works fine so these can be used together to take the place of the paired usage of .onAppear and .onDisappear until there is a fix for .onAppear

(Tagging this for some poor soul who had my problem: Camera indicator turning on in view that doesn't use camera SwiftUI)

iOS 14 .onAppear() is called on DISappear instead of appear
 
 
Q