Inconsistency on view lifecycle events between UIKit and SwiftUI when using UIVPageViewController

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)

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.

Inconsistency on view lifecycle events between UIKit and SwiftUI when using UIVPageViewController
 
 
Q