Overview
I've found inconsistency on view lifecycle events between UIKit and SwiftUI as the following shows when using UIVPageViewController and UIHostingController as one of its pages.
- SwiftUI View
onAppear
is only called at the first time to display and never called in the other cases.
- UIViewController
viewDidAppear
is not called at the first time to display, but it's called when the page view controller changes its page displayed.
The whole view structure is as follows:
- UIViewController (root)
- UIPageViewController (as its container view)
- UIHostingController (as its page)
- SwiftUI View (as its content view)
- UIViewControllerRepresentable (as a part of its body)
- UIViewController (as its content)
- UIViewControllerRepresentable (as a part of its body)
- SwiftUI View (as its content view)
- UIHostingController (as its page)
- UIPageViewController (as its container view)
Environment
- Xcode Version 15.4 (15F31d)
- iPhone 15 Pro (iOS 17.5) (Simulator)
- iPhone 8 (iOS 15.0) (Simulator)
Sample code
import UIKit
import SwiftUI
class ViewController: UIViewController, UIPageViewControllerDelegate, UIPageViewControllerDataSource {
private var pageViewController: UIPageViewController!
private var viewControllers: [UIViewController] = []
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup() {
pageViewController.delegate = self
pageViewController.dataSource = self
let page1 = UIHostingController(rootView: MainPageView())
let page2 = UIViewController()
page2.view.backgroundColor = .systemBlue
let page3 = UIViewController()
page3.view.backgroundColor = .systemGreen
viewControllers = [page1, page2, page3]
pageViewController.setViewControllers([page1], direction: .forward, animated: false)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
super.prepare(for: segue, sender: sender)
guard let pageViewController = segue.destination as? UIPageViewController else { return }
self.pageViewController = pageViewController
}
func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
print("debug: \(#function)")
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
print("debug: \(#function)")
guard let viewControllerIndex = viewControllers.firstIndex(of: viewController) else { return nil }
let previousIndex = viewControllerIndex - 1
guard previousIndex >= 0, viewControllers.count > previousIndex else { return nil }
return viewControllers[previousIndex]
}
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
print("debug: \(#function)")
guard let viewControllerIndex = viewControllers.firstIndex(of: viewController) else { return nil }
let nextIndex = viewControllerIndex + 1
guard viewControllers.count != nextIndex, viewControllers.count > nextIndex else { return nil }
return viewControllers[nextIndex]
}
}
struct MainPageView: View {
var body: some View {
VStack(spacing: 0) {
PageContentView()
PageFooterView()
}
.onAppear { print("debug: \(type(of: Self.self)) onAppear") }
.onDisappear { print("debug: \(type(of: Self.self)) onDisappear") }
}
}
struct PageFooterView: View {
var body: some View {
Text("PageFooterView")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.onAppear { print("debug: \(type(of: Self.self)) onAppear") }
.onDisappear { print("debug: \(type(of: Self.self)) onDisappear") }
}
}
struct PageContentView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> some UIViewController {
PageContentViewController()
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {}
}
class PageContentViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
setup()
}
private func setup() {
view.backgroundColor = .systemYellow
let label = UILabel()
label.text = "PageContentViewController"
label.font = .preferredFont(forTextStyle: .title1)
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.centerXAnchor.constraint(equalTo: view.centerXAnchor),
label.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("debug: \(type(of: Self.self)) \(#function)")
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
print("debug: \(type(of: Self.self)) \(#function)")
}
}
Logs
// Display the views
debug: MainPageView.Type onAppear
debug: PageFooterView.Type onAppear
// Swipe to the next page
debug: pageViewController(_:viewControllerAfter:)
debug: pageViewController(_:viewControllerBefore:)
debug: PageContentViewController.Type viewDidDisappear(_:)
debug: pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:)
debug: pageViewController(_:viewControllerAfter:)
// Swipe to the previous page
debug: PageContentViewController.Type viewDidAppear(_:)
debug: pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:)
debug: pageViewController(_:viewControllerBefore:)
As you can see here, onAppear
is only called at the first time to display but never called in the other cases while viewDidAppear
is the other way around.