248 lines
8.9 KiB
Swift
248 lines
8.9 KiB
Swift
import Foundation
|
|
import UIKit
|
|
|
|
/// @class RNSSplitViewScreenController
|
|
/// @brief A UIViewController subclass that manages a SplitView column in a UISplitViewController.
|
|
///
|
|
/// Associated with a RNSSplitViewScreenComponentView, it handles layout synchronization with the
|
|
/// Shadow Tree, emits React lifecycle events, and interacts with the SplitViewHost hierarchy.
|
|
@objc
|
|
public class RNSSplitViewScreenController: UIViewController {
|
|
let splitViewScreenComponentView: RNSSplitViewScreenComponentView
|
|
|
|
private var shadowStateProxy: RNSSplitViewScreenShadowStateProxy {
|
|
return splitViewScreenComponentView.shadowStateProxy()
|
|
}
|
|
|
|
private var reactEventEmitter: RNSSplitViewScreenComponentEventEmitter {
|
|
return splitViewScreenComponentView.reactEventEmitter()
|
|
}
|
|
|
|
private var viewSizeTransitionState: ViewSizeTransitionState? = nil
|
|
|
|
@objc public required init(splitViewScreenComponentView: RNSSplitViewScreenComponentView) {
|
|
self.splitViewScreenComponentView = splitViewScreenComponentView
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
return nil
|
|
}
|
|
|
|
///
|
|
/// @brief Searching for the SplitViewHost controller
|
|
///
|
|
/// It checks whether the parent controller is our host controller.
|
|
/// If we're outside the structure, e. g. for inspector represented as a modal,
|
|
/// we're searching for that controller using a reference that Screen keeps for Host component view.
|
|
///
|
|
/// @return If found - a RNSSplitViewHostController instance, otherwise nil.
|
|
///
|
|
func findSplitViewHostController() -> RNSSplitViewHostController? {
|
|
if let splitViewHostController = self.splitViewController as? RNSSplitViewHostController {
|
|
return splitViewHostController
|
|
}
|
|
|
|
if let splitViewHost = self.splitViewScreenComponentView.splitViewHost {
|
|
return splitViewHost.splitViewHostController
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
///
|
|
/// @brief Determines if this controller is nested inside a SplitViewHost hierarchy.
|
|
///
|
|
/// Used to differentiate between screens embedded in the native host and modal presentations.
|
|
///
|
|
/// @return true if inside RNSSplitViewHostController, false otherwise.
|
|
///
|
|
@objc
|
|
public func isInSplitViewHostSubtree() -> Bool {
|
|
return self.splitViewController is RNSSplitViewHostController
|
|
}
|
|
|
|
///
|
|
/// @brief Determines whether an SplitView animated transition is currently running
|
|
///
|
|
/// Used to differentiate favor frames from the presentation layer over view's frame .
|
|
///
|
|
/// @return true if the transition is running, false otherwise.
|
|
///
|
|
@objc
|
|
public func isViewSizeTransitionInProgress() -> Bool {
|
|
return viewSizeTransitionState != nil
|
|
}
|
|
|
|
// MARK: Signals
|
|
|
|
@objc
|
|
public func setNeedsLifecycleStateUpdate() {
|
|
findSplitViewHostController()?.setNeedsUpdateOfChildViewControllers()
|
|
}
|
|
|
|
// MARK: Layout
|
|
|
|
///
|
|
/// @brief This method is overridden to extract the value to which we're transitioning
|
|
/// and attach the DisplayLink to track frame updates on the presentation layer.
|
|
///
|
|
public override func viewWillTransition(
|
|
to size: CGSize,
|
|
with coordinator: any UIViewControllerTransitionCoordinator
|
|
) {
|
|
super.viewWillTransition(to: size, with: coordinator)
|
|
|
|
viewSizeTransitionState = ViewSizeTransitionState()
|
|
|
|
coordinator.animate(
|
|
alongsideTransition: { [weak self] context in
|
|
guard let self = self else { return }
|
|
guard let viewSizeTransitionState = self.viewSizeTransitionState else { return }
|
|
|
|
if viewSizeTransitionState.displayLink == nil {
|
|
viewSizeTransitionState.setupDisplayLink(
|
|
forTarget: self, selector: #selector(trackTransitionProgress))
|
|
}
|
|
},
|
|
completion: { [weak self] context in
|
|
guard let self = self else { return }
|
|
self.cleanupViewSizeTransitionState()
|
|
// After the animation completion, ensure that ShadowTree state
|
|
// is calculated relatively to the ancestor's frame by requesting
|
|
// the state update.
|
|
self.updateShadowTreeState()
|
|
})
|
|
}
|
|
|
|
private func cleanupViewSizeTransitionState() {
|
|
viewSizeTransitionState?.invalidate()
|
|
viewSizeTransitionState = nil
|
|
}
|
|
|
|
///
|
|
/// @brief This method is responsible for tracking animation frames and requests layout
|
|
/// which will synchronize ShadowNode size with the animation frame size.
|
|
///
|
|
@objc
|
|
private func trackTransitionProgress() {
|
|
if let currentFrame = view.layer.presentation()?.frame {
|
|
viewSizeTransitionState?.lastViewPresentationFrame = currentFrame
|
|
updateShadowTreeState()
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
|
|
updateShadowTreeState()
|
|
}
|
|
|
|
///
|
|
/// @brief Handles frame layout changes and updates Shadow Tree accordingly.
|
|
///
|
|
/// Requests for the ShadowNode updates through the shadow state proxy.
|
|
/// Differentiates cases when we're in the Host hierarchy to calculate frame relatively
|
|
/// to the Host view from the modal case where we're passing absolute layout metrics to the ShadowNode.
|
|
///
|
|
/// Prefers to apply dynamic updates from the presentation layer if the transition is in progress.
|
|
///
|
|
private func updateShadowTreeState() {
|
|
// For modals, which are presented outside the SplitViewHost subtree (and RN hierarchy),
|
|
// we're attaching our touch handler and we don't need to apply any offset corrections,
|
|
// because it's positioned relatively to our RNSSplitViewScreenComponentView
|
|
if !isInSplitViewHostSubtree() {
|
|
shadowStateProxy.updateShadowState(ofComponent: splitViewScreenComponentView)
|
|
return
|
|
}
|
|
|
|
let ancestorView = findSplitViewHostController()?.view
|
|
assert(
|
|
ancestorView != nil,
|
|
"[RNScreens] Expected to find RNSSplitViewHost component for RNSSplitViewScreen component"
|
|
)
|
|
|
|
// If the resize animation is currently running, we prefer to apply dynamic updates,
|
|
// based on the results from the presentation layer
|
|
// which is read from `trackTransitionProgress` method.
|
|
if let lastViewPresentationFrame = viewSizeTransitionState?.lastViewPresentationFrame,
|
|
!lastViewPresentationFrame.isNull
|
|
{
|
|
shadowStateProxy.updateShadowState(
|
|
ofComponent: splitViewScreenComponentView, withFrame: lastViewPresentationFrame,
|
|
inContextOfAncestorView: ancestorView!)
|
|
return
|
|
}
|
|
|
|
// There might be the case, when transition is about to start and in the meantime,
|
|
// sth else is triggering frame update relatively to the parent. As we know
|
|
// that dynamic updates from the presentation layer are coming, we're blocking this
|
|
// to prevent interrupting with the frames that are less important for us.
|
|
// This works fine, because after the animation completion, we're sending the last update
|
|
// which is compatible with the frame which would be calculated relatively to the ancestor here.
|
|
if !isViewSizeTransitionInProgress() {
|
|
shadowStateProxy.updateShadowState(
|
|
ofComponent: splitViewScreenComponentView, inContextOfAncestorView: ancestorView)
|
|
}
|
|
}
|
|
|
|
///
|
|
/// @brief Request ShadowNode state update when the SplitView screen frame origin has changed.
|
|
///
|
|
/// If there's a transition in progress, this function is ignored as we prefer to apply updates
|
|
/// that are dynamically coming from the presentation layer, rather than reading the frame, because
|
|
/// view's frame is set to the target value at the begining of the transition.
|
|
///
|
|
/// @param splitViewController The UISplitViewController whose layout positioning changed, represented by RNSSplitViewHostController.
|
|
///
|
|
func columnPositioningDidChangeIn(splitViewController: UISplitViewController) {
|
|
// During the transition, we're listening for the animation
|
|
// frame updates on the presentation layer and we're
|
|
// treating these updates as the source of truth
|
|
if !isViewSizeTransitionInProgress() {
|
|
shadowStateProxy.updateShadowState(
|
|
ofComponent: splitViewScreenComponentView, inContextOfAncestorView: splitViewController.view
|
|
)
|
|
}
|
|
}
|
|
|
|
// MARK: Events
|
|
|
|
public override func viewWillAppear(_ animated: Bool) {
|
|
reactEventEmitter.emitOnWillAppear()
|
|
}
|
|
|
|
public override func viewDidAppear(_ animated: Bool) {
|
|
reactEventEmitter.emitOnDidAppear()
|
|
}
|
|
|
|
public override func viewWillDisappear(_ animated: Bool) {
|
|
reactEventEmitter.emitOnWillDisappear()
|
|
}
|
|
|
|
public override func viewDidDisappear(_ animated: Bool) {
|
|
reactEventEmitter.emitOnDidDisappear()
|
|
}
|
|
}
|
|
|
|
private class ViewSizeTransitionState {
|
|
public var displayLink: CADisplayLink?
|
|
public var lastViewPresentationFrame: CGRect = CGRect.null
|
|
|
|
public func setupDisplayLink(forTarget target: Any, selector sel: Selector) {
|
|
if displayLink != nil {
|
|
displayLink?.invalidate()
|
|
}
|
|
|
|
displayLink = CADisplayLink(target: target, selector: sel)
|
|
displayLink!.add(to: .main, forMode: .common)
|
|
}
|
|
|
|
public func invalidate() {
|
|
displayLink?.invalidate()
|
|
displayLink = nil
|
|
lastViewPresentationFrame = CGRect.null
|
|
}
|
|
}
|