ssap_app/node_modules/react-native-screens/ios/gamma/split-view/RNSSplitViewScreenControlle...

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
}
}