JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
15.86k stars 1.15k forks source link

Some questions and ideas about the CMPViewController #4432

Open xinzhengzhang opened 6 months ago

xinzhengzhang commented 6 months ago

Describe the problem

Discussion on the implementation of CMPViewController

  1. Many system components, such as UIPageViewController, UITabViewController, etc., all use a single ViewController mode, which also means that other ViewControllers that are not on the screen will be moved off the screen. At present, it is checked through a 0.5 second rotation to dispose if they are not there. Regarding this piece, it is actually inconvenient to use in many scenarios. Is this the design so that you want to save the uistate externally to pass it to the ComposeContainer? If not, can there be a better way to deal with it?

  2. At present, the poll is started in viewWillAppear, which means that at least the page will be dispose with a latency of 0.5 seconds. At the same time, I feel that poll is not a very elegant solution. I do know that there will be many problems with iOS lifecycle, such as incorrect distribution of lifecycles, such as problems that may still exist after viewDidAppear, so can we use some more valid mechanisms and use the current poll as a downgrade solution?

  3. Even crashes when container reappeared in window(See video) https://github.com/JetBrains/compose-multiplatform/assets/1487445/b1859760-390d-4ece-816a-12f89258371b

Proposal Increase ability to control lifecycle of ComposeContainer

  1. Add an arbitrary NSObject (runtime) object to the ComposeContainer as optional params in constructor named lifecycleOwner
  2. Use objc_runtime mounting an new onDisposeTrigger object to the owner
  3. CMPViewController weak reference onDisposeTrigger object, notify CMPViewController onDispose when onDisposeTrigger dealloc occurs.
  4. If the owner does not exist, downgrade back to the original poll logic
//pseudocode

class OnDisposeTrigger {
     let onDispose: () -> Void
     dealloc {
          onDispose()
     }
}

class CMPViewController {
    let lifecycleOwner: NSObject?
    func transitLifecycleToStarted() {
        if lifecycleOwner {
             objc_setAssociatedObject(lifecycleOwner, OnDisposeTrigger { [weak self] in
                   self?.viewControllerDidLeaveWindowHierarchy()
            })
        } else { //scheduleHierarchyContainmentCheck }
   }
}

Affected platforms

Versions

Reproduction steps

  1. Below code will recompose every time when vc4 reentered
import UIKit
import Bridge

class ViewPagerController: UIViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {

    var pageViewController: UIPageViewController!
    var viewControllersList: [UIViewController] = []
    var segmentedControl: UISegmentedControl!

    override func viewDidLoad() {
        super.viewDidLoad()

        // Initialize the view controllers to display
        let vc1 = UIViewController()
        vc1.view.backgroundColor = .red
        let vc2 = UIViewController()
        vc2.view.backgroundColor = .blue
        let vc3 = UIViewController()
        vc3.view.backgroundColor = .green
        let vc4 = Compose_view_iosKt.getDemoViewController {[weak self] idle in
            for view in self?.pageViewController.view.subviews ?? [] {
                if let scrollView = view as? UIScrollView {
                    scrollView.isScrollEnabled = idle.boolValue
                }
            }
        }
        // vc4.view.isExclusiveTouch = true
        // let vc4 = UIViewController()
        // vc4.view.backgroundColor = .yellow
        let vc5: UIViewController = {
            let vc = UIViewController()
            vc.view.backgroundColor = .orange
            return vc
        }()

        self.addChild(vc4)

        viewControllersList.append(vc1)
        viewControllersList.append(vc2)
        viewControllersList.append(vc3)
        viewControllersList.append(vc4)
        viewControllersList.append(vc5)

        // Create the page view controller
        pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: nil)
        pageViewController.dataSource = self
        pageViewController.delegate = self

        // Set the first view controller to display
        pageViewController.setViewControllers([viewControllersList[0]], direction: .forward, animated: true, completion: nil)

        // Add the page view controller to the current view controller
        addChild(pageViewController)
        view.addSubview(pageViewController.view)
        pageViewController.didMove(toParent: self)

        // Set the page view controller's bounds
        pageViewController.view.frame = view.bounds

        // Create the segmented control
        segmentedControl = UISegmentedControl(items: ["First", "Second", "Third", "Compose", "Fifth"])
        segmentedControl.selectedSegmentIndex = 0
        segmentedControl.addTarget(self, action: #selector(segmentedControlValueChanged), for: .valueChanged)
        navigationItem.titleView = segmentedControl
    }

    @objc func segmentedControlValueChanged(_ sender: UISegmentedControl) {
        let direction: UIPageViewController.NavigationDirection = sender.selectedSegmentIndex > (pageViewController.viewControllers?.first?.view.tag ?? 0) ? .forward : .reverse
        pageViewController.setViewControllers([viewControllersList[sender.selectedSegmentIndex]], direction: direction, animated: true, completion: nil)
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        guard let index = viewControllersList.firstIndex(of: viewController), index > 0 else {
            return nil
        }
        return viewControllersList[index - 1]
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        guard let index = viewControllersList.firstIndex(of: viewController), index < (viewControllersList.count - 1) else {
            return nil
        }
        return viewControllersList[index + 1]
    }

    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        if completed, let currentViewController = pageViewController.viewControllers?.first, let index = viewControllersList.firstIndex(of: currentViewController) {
            segmentedControl.selectedSegmentIndex = index
        }
    }
}
igordmn commented 6 months ago

Thanks!

@elijah-semyonov, could you look at the suggestions? Let's discuss them next week?

elijah-semyonov commented 5 months ago

Thanks for the report. Basically what you propose is a way to extend lifetime and have a way to control it explicitly, making it "you will not destroyed earlier, than I tell you". We will consider it because the usage case seems legitimate.

okushnikov commented 1 month ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.