Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions ios/gamma/split-view/RNSSplitViewScreenComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -167,24 +167,6 @@ - (void)safeAreaInsetsDidChange
return react::concreteComponentDescriptorProvider<react::RNSSplitViewScreenComponentDescriptor>();
}

- (void)updateLayoutMetrics:(const facebook::react::LayoutMetrics &)layoutMetrics
oldLayoutMetrics:(const facebook::react::LayoutMetrics &)oldLayoutMetrics
{
// We're tracking presentation layer updates in the RNSSplitViewScreen.
// There's a problem with SplitView that it sets the frame to the end value of the animation right after the animation
// begins. Because of that, the size of our component is desynchronizing easily and we're blocking a communication
// between native and shadow layout for a while until the transition ends. For the following case when we want to make
// a transition from width A to B:
// 1. size 'A' is set on ShadowNode
// 2. animation for the transition starts
// 3. `setFrame` is called with the width 'B'
// 4. in the same time, we want to track updates and treat intermediate value A' indicated from the presentation layer
// as our source of truth
if (![_controller isViewSizeTransitionInProgress]) {
[super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics];
}
}

+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
Expand Down
124 changes: 5 additions & 119 deletions ios/gamma/split-view/RNSSplitViewScreenController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ public class RNSSplitViewScreenController: UIViewController {
return splitViewScreenComponentView.reactEventEmitter()
}

private var viewSizeTransitionState: ViewSizeTransitionState? = nil

@objc public required init(splitViewScreenComponentView: RNSSplitViewScreenComponentView) {
self.splitViewScreenComponentView = splitViewScreenComponentView
super.init(nibName: nil, bundle: nil)
Expand Down Expand Up @@ -62,18 +60,6 @@ public class RNSSplitViewScreenController: UIViewController {
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
Expand All @@ -83,55 +69,6 @@ public class RNSSplitViewScreenController: UIViewController {

// 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()
Expand All @@ -146,8 +83,6 @@ public class RNSSplitViewScreenController: UIViewController {
/// 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,
Expand All @@ -163,48 +98,19 @@ public class RNSSplitViewScreenController: UIViewController {
"[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)
}
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
)
}
shadowStateProxy.updateShadowState(
ofComponent: splitViewScreenComponentView, inContextOfAncestorView: splitViewController.view
)
}

// MARK: Events
Expand Down Expand Up @@ -237,23 +143,3 @@ extension RNSSplitViewScreenController: RNSFrameCorrectionProvider {
self.splitViewScreenComponentView.unregister(fromFrameCorrection: view)
}
}

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