first commit

This commit is contained in:
2026-03-10 16:18:05 +00:00
commit 11f9c069b5
31635 changed files with 3187747 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
///
/// @brief - Class responsible for applying all upcoming updates to SplitView.
///
/// This class is synchronizing UISplitViewController configuration props which are affecting the SplitView appearance with props passed to RNSSplitViewHostComponentView from the ElementTree.
///
class RNSSplitViewAppearanceApplicator {
///
/// @brief Function responsible for applying all updates to SplitView in correct order
///
/// It requests calling proper callbacks with batched SplitView updates on the AppearanceCoordinator object
///
/// @param splitView The view representing JS component which is sending updates.
/// @param splitViewController The controller associated with the SplitView component which receives updates and manages the native layer.
/// @param appearanceCoordinator The coordinator which is checking whether the update needs to be applied and if so, it executes the callback passed by this class.
///
public func updateAppearanceIfNeeded(
_ splitView: RNSSplitViewHostComponentView,
_ splitViewController: RNSSplitViewHostController,
_ appearanceCoordinator: RNSSplitViewAppearanceCoordinator
) {
appearanceCoordinator.updateIfNeeded(.generalUpdate) { [weak self] in
guard let self = self
else {
return
}
self.updateSplitViewConfiguration(for: splitView, withController: splitViewController)
}
appearanceCoordinator.updateIfNeeded(.secondaryScreenNavBarUpdate) { [weak self] in
guard let self = self
else {
return
}
splitViewController.refreshSecondaryNavBar()
}
appearanceCoordinator.updateIfNeeded(.displayModeUpdate) { [weak self] in
guard let self = self
else {
return
}
self.updateSplitViewDisplayMode(for: splitView, withController: splitViewController)
}
appearanceCoordinator.updateIfNeeded(.orientationUpdate) { [] in
RNSScreenWindowTraits.enforceDesiredDeviceOrientation()
}
}
///
/// @brief Function that applies all basic updates.
///
/// It calls all setters on RNSSplitViewHostController that doesn't require any custom logic and conditions to be met.
///
/// @param splitView The view representing JS component which is sending updates.
/// @param splitViewController The controller associated with the SplitView component which receives updates and manages the native layer.
///
private func updateSplitViewConfiguration(
for splitView: RNSSplitViewHostComponentView,
withController splitViewController: RNSSplitViewHostController
) {
// Step 1 - general settings
splitViewController.displayModeButtonVisibility = splitView.displayModeButtonVisibility
splitViewController.preferredSplitBehavior = splitView.preferredSplitBehavior
#if !os(tvOS)
splitViewController.primaryBackgroundStyle = splitView.primaryBackgroundStyle
#endif
splitViewController.presentsWithGesture = splitView.presentsWithGesture
splitViewController.primaryEdge = splitView.primaryEdge
splitViewController.showsSecondaryOnlyButton = splitView.showSecondaryToggleButton
// Step 2.1 - validating column constraints
validateColumnConstraints(
minWidth: splitView.minimumPrimaryColumnWidth,
maxWidth: splitView.maximumPrimaryColumnWidth)
validateColumnConstraints(
minWidth: splitView.minimumSupplementaryColumnWidth,
maxWidth: splitView.maximumSupplementaryColumnWidth)
#if compiler(>=6.2) && !os(tvOS)
if #available(iOS 26.0, *) {
validateColumnConstraints(
minWidth: splitView.minimumInspectorColumnWidth,
maxWidth: splitView.maximumInspectorColumnWidth)
}
#endif
// Step 2.2 - applying updates to columns
if splitView.minimumPrimaryColumnWidth >= 0 {
splitViewController.minimumPrimaryColumnWidth = splitView.minimumPrimaryColumnWidth
}
if splitView.maximumPrimaryColumnWidth >= 0 {
splitViewController.maximumPrimaryColumnWidth = splitView.maximumPrimaryColumnWidth
}
if splitView.preferredPrimaryColumnWidthOrFraction >= 0
&& splitView.preferredPrimaryColumnWidthOrFraction < 1
{
splitViewController.preferredPrimaryColumnWidthFraction =
splitView.preferredPrimaryColumnWidthOrFraction
} else if splitView.preferredPrimaryColumnWidthOrFraction >= 1 {
splitViewController.preferredPrimaryColumnWidth =
splitView.preferredPrimaryColumnWidthOrFraction
}
if splitView.minimumSupplementaryColumnWidth >= 0 {
splitViewController.minimumSupplementaryColumnWidth =
splitView.minimumSupplementaryColumnWidth
}
if splitView.maximumSupplementaryColumnWidth >= 0 {
splitViewController.maximumSupplementaryColumnWidth =
splitView.maximumSupplementaryColumnWidth
}
if splitView.preferredSupplementaryColumnWidthOrFraction >= 0
&& splitView.preferredSupplementaryColumnWidthOrFraction < 1
{
splitViewController.preferredSupplementaryColumnWidthFraction =
splitView.preferredSupplementaryColumnWidthOrFraction
} else if splitView.preferredSupplementaryColumnWidthOrFraction >= 1 {
splitViewController.preferredSupplementaryColumnWidth =
splitView.preferredSupplementaryColumnWidthOrFraction
}
#if compiler(>=6.2) && !os(tvOS)
if #available(iOS 26.0, *) {
if splitView.minimumSecondaryColumnWidth >= 0 {
splitViewController.minimumSecondaryColumnWidth = splitView.minimumSecondaryColumnWidth
}
if splitView.preferredSecondaryColumnWidthOrFraction >= 0
&& splitView.preferredSecondaryColumnWidthOrFraction < 1
{
splitViewController.preferredSecondaryColumnWidthFraction =
splitView.preferredSecondaryColumnWidthOrFraction
} else if splitView.preferredSecondaryColumnWidthOrFraction >= 1 {
splitViewController.preferredSecondaryColumnWidth =
splitView.preferredSecondaryColumnWidthOrFraction
}
if splitView.minimumInspectorColumnWidth >= 0 {
splitViewController.minimumInspectorColumnWidth = splitView.minimumInspectorColumnWidth
}
if splitView.maximumInspectorColumnWidth >= 0 {
splitViewController.maximumInspectorColumnWidth = splitView.maximumInspectorColumnWidth
}
if splitView.preferredInspectorColumnWidthOrFraction >= 0
&& splitView.preferredInspectorColumnWidthOrFraction < 1
{
splitViewController.preferredInspectorColumnWidthFraction =
splitView.preferredInspectorColumnWidthOrFraction
} else if splitView.preferredInspectorColumnWidthOrFraction >= 1 {
splitViewController.preferredInspectorColumnWidth =
splitView.preferredInspectorColumnWidthOrFraction
}
}
#endif
// Step 2.3 - manipulating with inspector column
splitViewController.toggleSplitViewInspector(splitView.showInspector)
}
///
/// @brief Function that updates `preferredDisplayMode` property on SplitView.
///
/// `preferredDisplayMode` needs to have a dedicated flag to prevent updates from the JS, when other props updates the appearance.
/// It is crucial in the case, when `preferredDisplayMode` has changed due to some transition that was executed natively, e. g. after showing/hiding a column by a swipe.
/// In that case, any prop update incoming, would reset `preferredDisplayMode` to the state from JS, what doesn't look good.
///
/// @param splitView The view representing JS component which is sending updates.
/// @param splitViewController The controller associated with the SplitView component which receives updates and manages the native layer.
///
func updateSplitViewDisplayMode(
for splitView: RNSSplitViewHostComponentView,
withController splitViewController: RNSSplitViewHostController
) {
splitViewController.preferredDisplayMode = splitView.preferredDisplayMode
}
func validateColumnConstraints(minWidth: CGFloat, maxWidth: CGFloat) {
assert(
minWidth <= maxWidth,
"[RNScreens] SplitView column constraints are invalid: minWidth \(minWidth) cannot be greater than maxWidth \(maxWidth)"
)
}
}

View File

@@ -0,0 +1,26 @@
///
/// @brief - A class that is responsible for coordinating SplitViewHost appearance updates.
///
/// It collects flags for SplitView appearance update actions and invalidates them.
/// It's also responsible for executing callbacks when the action is requested.
///
final class RNSSplitViewAppearanceCoordinator {
var updateFlags: RNSSplitViewAppearanceUpdateFlags = []
public func needs(_ updateFlag: RNSSplitViewAppearanceUpdateFlags) {
updateFlags.insert(updateFlag)
}
public func updateIfNeeded(
_ updateFlag: RNSSplitViewAppearanceUpdateFlags, _ updateCallback: () -> Void
) {
if isNeeded(updateFlag) {
updateFlags.remove(updateFlag)
updateCallback()
}
}
func isNeeded(_ updateFlag: RNSSplitViewAppearanceUpdateFlags) -> Bool {
updateFlags.contains(updateFlag)
}
}

View File

@@ -0,0 +1,11 @@
///
/// @brief - A collection of flags, which can be invalidated on RNSSplitViewHostController to apply proper updates to SplitView
///
struct RNSSplitViewAppearanceUpdateFlags: OptionSet {
let rawValue: UInt8
static let generalUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 0)
static let secondaryScreenNavBarUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 1)
static let displayModeUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 2)
static let orientationUpdate = RNSSplitViewAppearanceUpdateFlags(rawValue: 1 << 3)
}

View File

@@ -0,0 +1,70 @@
#pragma once
#import <Foundation/Foundation.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus)
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // __cplusplus
NS_ASSUME_NONNULL_BEGIN
/**
* @class RNSSplitViewHostComponentEventEmitter
* @brief Responsible for emitting events from the native SplitView to the React Element Tree.
*/
@interface RNSSplitViewHostComponentEventEmitter : NSObject
/**
* @brief Emits the onCollapse event to notify associated component instance that the SplitView has collapsed.
*
* Call this method when the SplitView collapses to a single column e.g. due to
* size constraints or user interaction to notify React realm of this event.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnCollapse;
- (BOOL)emitOnDisplayModeWillChangeFrom:(UISplitViewControllerDisplayMode)currentDisplayMode
to:(UISplitViewControllerDisplayMode)nextDisplayMode;
/**
* @brief Emits the onExpand event to notify associated component instance that the SplitView has expanded.
*
* Call this method when a SplitView is transitioning from the collapsed state to the multi-column display
* to notify React realm of this event.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnExpand;
/**
* @brief Emits the onHideInspector event to notify that the inspector column is being hidden.
*
* Call this method when a user dismisses an inspector modal or hides it programmatically
* to notify React for synchronize the state based on this event.
*
* @return true if the event was emitted successfully, false otherwise.
*/
- (BOOL)emitOnHideInspector;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSSplitViewHostComponentEventEmitter ()
/**
* @brief Sets the underlying C++ event emitter used for dispatching events to React.
*
* @param emitter A shared pointer to a RNSSplitViewHostEventEmitter instance.
*/
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSSplitViewHostEventEmitter> &)emitter;
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,62 @@
#import "RNSSplitViewHostComponentEventEmitter.h"
#import <React/RCTConversions.h>
#import <React/RCTLog.h>
#import "RNSConversions.h"
@implementation RNSSplitViewHostComponentEventEmitter {
std::shared_ptr<const react::RNSSplitViewHostEventEmitter> _reactEventEmitter;
}
- (BOOL)emitOnCollapse
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onCollapse({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnCollapse event emission due to nullish emitter");
return NO;
}
}
- (BOOL)emitOnDisplayModeWillChangeFrom:(UISplitViewControllerDisplayMode)currentDisplayMode
to:(UISplitViewControllerDisplayMode)nextDisplayMode
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onDisplayModeWillChange(
{.currentDisplayMode = rnscreens::conversion::UISplitViewControllerDisplayModeToString(currentDisplayMode),
.nextDisplayMode = rnscreens::conversion::UISplitViewControllerDisplayModeToString(nextDisplayMode)});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDisplayModeWillChange event emission due to nullish emitter");
return NO;
}
}
- (BOOL)emitOnExpand
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onExpand({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnExpand event emission due to nullish emitter");
return NO;
}
}
- (BOOL)emitOnHideInspector
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onInspectorHide({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnHideInspector event emission due to nullish emitter");
return NO;
}
}
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSSplitViewHostEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
@end

View File

@@ -0,0 +1,78 @@
#pragma once
#import "RNSDefines.h"
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#import "RNSSplitViewHostComponentEventEmitter.h"
NS_ASSUME_NONNULL_BEGIN
@class RNSSplitViewHostController;
@class RNSSplitViewScreenComponentView;
/**
* @class RNSSplitViewHostComponentView
* @brief A view component representing top-level native component for SplitView.
*
* Responsible for managing multi-column layouts via associated native UISplitViewController.
* Manages updates to the layout properties, column configuration, and event emission.
*/
@interface RNSSplitViewHostComponentView : RNSReactBaseView
- (nonnull NSMutableArray<RNSSplitViewScreenComponentView *> *)reactSubviews;
@property (nonatomic, nonnull, strong, readonly) RNSSplitViewHostController *splitViewHostController;
@end
#pragma mark - Props
/**
* @category Props
* @brief Definitions for React Native props.
*/
@interface RNSSplitViewHostComponentView ()
@property (nonatomic, readonly) UISplitViewControllerSplitBehavior preferredSplitBehavior;
@property (nonatomic, readonly) UISplitViewControllerPrimaryEdge primaryEdge;
@property (nonatomic, readonly) UISplitViewControllerDisplayMode preferredDisplayMode;
@property (nonatomic, readonly) UISplitViewControllerDisplayModeButtonVisibility displayModeButtonVisibility;
#if !TARGET_OS_TV
@property (nonatomic, readonly) UISplitViewControllerBackgroundStyle primaryBackgroundStyle;
#endif // !TARGET_OS_TV
@property (nonatomic, readonly) BOOL presentsWithGesture;
@property (nonatomic, readonly) BOOL showSecondaryToggleButton;
@property (nonatomic, readonly) BOOL showInspector;
@property (nonatomic, readonly) double minimumPrimaryColumnWidth;
@property (nonatomic, readonly) double maximumPrimaryColumnWidth;
@property (nonatomic, readonly) double preferredPrimaryColumnWidthOrFraction;
@property (nonatomic, readonly) double minimumSupplementaryColumnWidth;
@property (nonatomic, readonly) double maximumSupplementaryColumnWidth;
@property (nonatomic, readonly) double preferredSupplementaryColumnWidthOrFraction;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
@property (nonatomic, readonly) double minimumSecondaryColumnWidth;
@property (nonatomic, readonly) double preferredSecondaryColumnWidthOrFraction;
@property (nonatomic, readonly) double minimumInspectorColumnWidth;
@property (nonatomic, readonly) double maximumInspectorColumnWidth;
@property (nonatomic, readonly) double preferredInspectorColumnWidthOrFraction;
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
@property (nonatomic, readonly) RNSOrientation orientation;
@end
#pragma mark - Events
/**
* @category Events
* @brief APIs related to event emission to React Native.
*/
@interface RNSSplitViewHostComponentView ()
- (nonnull RNSSplitViewHostComponentEventEmitter *)reactEventEmitter;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,398 @@
#import "RNSSplitViewHostComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTMountingTransactionObserving.h>
#import <React/UIView+React.h>
#import <react/renderer/components/rnscreens/ComponentDescriptors.h>
#import "RNSConversions.h"
#import "RNSDefines.h"
#import "RNSSplitViewScreenComponentView.h"
#import "Swift-Bridging.h"
namespace react = facebook::react;
static const CGFloat epsilon = 1e-6;
#define COLUMN_METRIC_CHANGED(OLD, NEW, PROPERTY_NAME, EPSILON) \
(fabs((OLD).columnMetrics.PROPERTY_NAME - (NEW).columnMetrics.PROPERTY_NAME) > (EPSILON))
@interface RNSSplitViewHostComponentView () <RCTMountingTransactionObserving>
@end
@implementation RNSSplitViewHostComponentView {
RNSSplitViewHostComponentEventEmitter *_Nonnull _reactEventEmitter;
RNSSplitViewHostController *_Nonnull _controller;
NSMutableArray<RNSSplitViewScreenComponentView *> *_Nonnull _reactSubviews;
bool _hasModifiedReactSubviewsInCurrentTransaction;
bool _needsSplitViewAppearanceUpdate;
bool _needsSplitViewSecondaryScreenNavBarUpdate;
bool _needsSplitViewDisplayModeUpdate;
bool _needsSplitViewOrientationUpdate;
// We need this information to warn users about dynamic changes to behavior being currently unsupported.
bool _isShowSecondaryToggleButtonSet;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
- (void)initState
{
[self resetProps];
_reactEventEmitter = [RNSSplitViewHostComponentEventEmitter new];
_hasModifiedReactSubviewsInCurrentTransaction = false;
_needsSplitViewAppearanceUpdate = false;
_needsSplitViewSecondaryScreenNavBarUpdate = false;
_needsSplitViewDisplayModeUpdate = false;
_needsSplitViewOrientationUpdate = false;
_reactSubviews = [NSMutableArray new];
}
- (void)resetProps
{
static const auto defaultProps = std::make_shared<const react::RNSSplitViewHostProps>();
_props = defaultProps;
_preferredSplitBehavior = UISplitViewControllerSplitBehaviorAutomatic;
_primaryEdge = UISplitViewControllerPrimaryEdgeLeading;
_preferredDisplayMode = UISplitViewControllerDisplayModeAutomatic;
_displayModeButtonVisibility = UISplitViewControllerDisplayModeButtonVisibilityAutomatic;
#if !TARGET_OS_TV
UISplitViewController *tempSplitVC = [[UISplitViewController alloc] init];
_primaryBackgroundStyle = tempSplitVC.primaryBackgroundStyle;
#endif // !TARGET_OS_TV
_presentsWithGesture = true;
_showSecondaryToggleButton = false;
_showInspector = false;
_minimumPrimaryColumnWidth = -1.0;
_maximumPrimaryColumnWidth = -1.0;
_preferredPrimaryColumnWidthOrFraction = -1.0;
_minimumSupplementaryColumnWidth = -1.0;
_maximumSupplementaryColumnWidth = -1.0;
_preferredSupplementaryColumnWidthOrFraction = -1.0;
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
_minimumSecondaryColumnWidth = -1.0;
_preferredSecondaryColumnWidthOrFraction = -1.0;
_minimumInspectorColumnWidth = -1.0;
_maximumInspectorColumnWidth = -1.0;
_preferredInspectorColumnWidthOrFraction = -1.0;
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
_orientation = RNSOrientationInherit;
_isShowSecondaryToggleButtonSet = false;
}
- (int)getNumberOfColumns
{
int numberOfColumns = 0;
for (RNSSplitViewScreenComponentView *component in _reactSubviews) {
if (component.columnType == RNSSplitViewScreenColumnTypeColumn) {
numberOfColumns++;
}
}
return numberOfColumns;
}
- (void)setupController
{
// Controller needs to know about the number of reactSubviews before its initialization to pass proper number of
// columns to the constructor. Therefore, we must delay it's creation until attaching it to window.
// At this point, children are already attached to the Host component, therefore, we may create SplitView controller.
if (_controller == nil) {
int numberOfColumns = [self getNumberOfColumns];
_controller = [[RNSSplitViewHostController alloc] initWithSplitViewHostComponentView:self
numberOfColumns:numberOfColumns];
}
}
- (void)didMoveToWindow
{
[self setupController];
RCTAssert(_controller != nil, @"[RNScreens] Controller must not be nil while attaching to window");
[self requestSplitViewHostControllerForAppearanceUpdate];
[self reactAddControllerToClosestParent:_controller];
}
- (void)reactAddControllerToClosestParent:(UIViewController *)controller
{
if (!controller.parentViewController) {
UIView *parentView = (UIView *)self.reactSuperview;
while (parentView) {
if (parentView.reactViewController) {
[parentView.reactViewController addChildViewController:controller];
[self addSubview:controller.view];
[controller didMoveToParentViewController:parentView.reactViewController];
break;
}
parentView = (UIView *)parentView.reactSuperview;
}
return;
}
}
RNS_IGNORE_SUPER_CALL_BEGIN
- (nonnull NSMutableArray<RNSSplitViewScreenComponentView *> *)reactSubviews
{
RCTAssert(
_reactSubviews != nil,
@"[RNScreens] Attempt to work with non-initialized list of RNSSplitViewScreenComponentView subviews. (for: %@)",
self);
return _reactSubviews;
}
RNS_IGNORE_SUPER_CALL_END
- (nonnull RNSSplitViewHostController *)splitViewHostController
{
RCTAssert(_controller != nil, @"[RNScreens] Controller must not be nil");
return _controller;
}
#pragma mark - RCTComponentViewProtocol
- (void)mountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
[childComponentView isKindOfClass:RNSSplitViewScreenComponentView.class],
@"[RNScreens] Attempt to mount child of unsupported type: %@, expected %@",
childComponentView.class,
RNSSplitViewScreenComponentView.class);
auto *childScreen = static_cast<RNSSplitViewScreenComponentView *>(childComponentView);
childScreen.splitViewHost = self;
[_reactSubviews insertObject:childScreen atIndex:index];
_hasModifiedReactSubviewsInCurrentTransaction = true;
}
- (void)unmountChildComponentView:(UIView<RCTComponentViewProtocol> *)childComponentView index:(NSInteger)index
{
RCTAssert(
[childComponentView isKindOfClass:RNSSplitViewScreenComponentView.class],
@"[RNScreens] Attempt to unmount child of unsupported type: %@, expected %@",
childComponentView.class,
RNSSplitViewScreenComponentView.class);
auto *childScreen = static_cast<RNSSplitViewScreenComponentView *>(childComponentView);
childScreen.splitViewHost = nil;
[_reactSubviews removeObject:childScreen];
_hasModifiedReactSubviewsInCurrentTransaction = true;
}
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSSplitViewHostComponentDescriptor>();
}
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSSplitViewHostProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSSplitViewHostProps>(props);
if (oldComponentProps.preferredSplitBehavior != newComponentProps.preferredSplitBehavior) {
_needsSplitViewAppearanceUpdate = true;
_preferredSplitBehavior =
rnscreens::conversion::SplitViewPreferredSplitBehaviorFromHostProp(newComponentProps.preferredSplitBehavior);
}
if (oldComponentProps.primaryEdge != newComponentProps.primaryEdge) {
_needsSplitViewAppearanceUpdate = true;
_primaryEdge = rnscreens::conversion::SplitViewPrimaryEdgeFromHostProp(newComponentProps.primaryEdge);
}
if (oldComponentProps.preferredDisplayMode != newComponentProps.preferredDisplayMode) {
_needsSplitViewAppearanceUpdate = true;
_needsSplitViewDisplayModeUpdate = true;
_preferredDisplayMode =
rnscreens::conversion::SplitViewPreferredDisplayModeFromHostProp(newComponentProps.preferredDisplayMode);
}
#if !TARGET_OS_TV
if (oldComponentProps.primaryBackgroundStyle != newComponentProps.primaryBackgroundStyle) {
_needsSplitViewAppearanceUpdate = true;
_primaryBackgroundStyle =
rnscreens::conversion::SplitViewPrimaryBackgroundStyleFromHostProp(newComponentProps.primaryBackgroundStyle);
}
#endif // !TARGET_OS_TV
if (oldComponentProps.presentsWithGesture != newComponentProps.presentsWithGesture) {
_needsSplitViewAppearanceUpdate = true;
_presentsWithGesture = newComponentProps.presentsWithGesture;
}
if (oldComponentProps.showSecondaryToggleButton != newComponentProps.showSecondaryToggleButton) {
_needsSplitViewAppearanceUpdate = true;
_needsSplitViewSecondaryScreenNavBarUpdate = true;
_showSecondaryToggleButton = newComponentProps.showSecondaryToggleButton;
}
if (oldComponentProps.showInspector != newComponentProps.showInspector) {
_needsSplitViewAppearanceUpdate = true;
_showInspector = newComponentProps.showInspector;
}
if (oldComponentProps.displayModeButtonVisibility != newComponentProps.displayModeButtonVisibility) {
_needsSplitViewAppearanceUpdate = true;
_displayModeButtonVisibility = rnscreens::conversion::SplitViewDisplayModeButtonVisibilityFromHostProp(
newComponentProps.displayModeButtonVisibility);
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, minimumPrimaryColumnWidth, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_minimumPrimaryColumnWidth = newComponentProps.columnMetrics.minimumPrimaryColumnWidth;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, maximumPrimaryColumnWidth, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_maximumPrimaryColumnWidth = newComponentProps.columnMetrics.maximumPrimaryColumnWidth;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, preferredPrimaryColumnWidthOrFraction, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_preferredPrimaryColumnWidthOrFraction = newComponentProps.columnMetrics.preferredPrimaryColumnWidthOrFraction;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, minimumSupplementaryColumnWidth, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_minimumSupplementaryColumnWidth = newComponentProps.columnMetrics.minimumSupplementaryColumnWidth;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, maximumSupplementaryColumnWidth, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_maximumSupplementaryColumnWidth = newComponentProps.columnMetrics.maximumSupplementaryColumnWidth;
}
if (COLUMN_METRIC_CHANGED(
oldComponentProps, newComponentProps, preferredSupplementaryColumnWidthOrFraction, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_preferredSupplementaryColumnWidthOrFraction =
newComponentProps.columnMetrics.preferredSupplementaryColumnWidthOrFraction;
}
#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, minimumSecondaryColumnWidth, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_minimumSecondaryColumnWidth = newComponentProps.columnMetrics.minimumSecondaryColumnWidth;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, preferredSecondaryColumnWidthOrFraction, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_preferredSecondaryColumnWidthOrFraction = newComponentProps.columnMetrics.preferredSecondaryColumnWidthOrFraction;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, minimumInspectorColumnWidth, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_minimumInspectorColumnWidth = newComponentProps.columnMetrics.minimumInspectorColumnWidth;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, maximumInspectorColumnWidth, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_maximumInspectorColumnWidth = newComponentProps.columnMetrics.maximumInspectorColumnWidth;
}
if (COLUMN_METRIC_CHANGED(oldComponentProps, newComponentProps, preferredInspectorColumnWidthOrFraction, epsilon)) {
_needsSplitViewAppearanceUpdate = true;
_preferredInspectorColumnWidthOrFraction = newComponentProps.columnMetrics.preferredInspectorColumnWidthOrFraction;
}
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)
if (oldComponentProps.orientation != newComponentProps.orientation) {
_needsSplitViewOrientationUpdate = true;
_orientation = rnscreens::conversion::RNSOrientationFromRNSSplitViewHostOrientation(newComponentProps.orientation);
}
// This flag is set to true when showsSecondaryOnlyButton prop is assigned for the first time.
// This allows us to identify any subsequent changes to this prop,
// enabling us to warn users that dynamic changes are not supported.
_isShowSecondaryToggleButtonSet = true;
[super updateProps:props oldProps:oldProps];
}
- (void)finalizeUpdates:(RNComponentViewUpdateMask)updateMask
{
[self requestSplitViewHostControllerForAppearanceUpdate];
[super finalizeUpdates:updateMask];
}
- (void)requestSplitViewHostControllerForAppearanceUpdate
{
if (_needsSplitViewAppearanceUpdate && _controller != nil) {
_needsSplitViewAppearanceUpdate = false;
[_controller setNeedsAppearanceUpdate];
}
if (_needsSplitViewDisplayModeUpdate && _controller != nil) {
_needsSplitViewDisplayModeUpdate = false;
[_controller setNeedsDisplayModeUpdate];
}
if (_needsSplitViewSecondaryScreenNavBarUpdate && _controller != nil) {
_needsSplitViewSecondaryScreenNavBarUpdate = false;
[_controller setNeedsSecondaryScreenNavBarUpdate];
}
if (_needsSplitViewOrientationUpdate && _controller != nil) {
_needsSplitViewOrientationUpdate = false;
[_controller setNeedsOrientationUpdate];
}
}
#pragma mark - RCTMountingTransactionObserving
- (void)mountingTransactionWillMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
_hasModifiedReactSubviewsInCurrentTransaction = false;
[_controller reactMountingTransactionWillMount];
}
- (void)mountingTransactionDidMount:(const facebook::react::MountingTransaction &)transaction
withSurfaceTelemetry:(const facebook::react::SurfaceTelemetry &)surfaceTelemetry
{
if (_hasModifiedReactSubviewsInCurrentTransaction) {
[_controller setNeedsUpdateOfChildViewControllers];
}
[_controller reactMountingTransactionDidMount];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
[_reactEventEmitter
updateEventEmitter:std::static_pointer_cast<const react::RNSSplitViewHostEventEmitter>(eventEmitter)];
}
#pragma mark - Events
- (nonnull RNSSplitViewHostComponentEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
@end
Class<RCTComponentViewProtocol> RNSSplitViewHostCls(void)
{
return RNSSplitViewHostComponentView.class;
}
#undef COLUMN_METRIC_CHANGED

View File

@@ -0,0 +1,11 @@
#pragma once
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSSplitViewHostComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,7 @@
#import "RNSSplitViewHostComponentViewManager.h"
@implementation RNSSplitViewHostComponentViewManager
RCT_EXPORT_MODULE(RNSSplitViewHostComponentViewManager)
@end

View File

@@ -0,0 +1,509 @@
import Foundation
import UIKit
/// @class RNSSplitViewHostController
/// @brief A controller associated with the RN native component representing SplitView host.
///
/// Manages a collection of RNSSplitViewScreenComponentView instances,
/// synchronizes appearance settings with props, observes component lifecycle, and emits events.
@objc
public class RNSSplitViewHostController: UISplitViewController, ReactMountingTransactionObserving,
RNSOrientationProvidingSwift
{
private var needsChildViewControllersUpdate = false
private var splitViewAppearanceCoordinator: RNSSplitViewAppearanceCoordinator
private var splitViewAppearanceApplicator: RNSSplitViewAppearanceApplicator
private var reactEventEmitter: RNSSplitViewHostComponentEventEmitter {
return splitViewHostComponentView.reactEventEmitter()
}
private let splitViewHostComponentView: RNSSplitViewHostComponentView
/// This variable is keeping the value of how many columns were set in the initial render. It's used for validation, because SplitView doesn't support changing number of columns dynamically.
private let fixedColumnsCount: Int
private let minNumberOfColumns: Int = 2
private let maxNumberOfColumns: Int = 3
private let maxNumberOfInspectors: Int = 1
/// Tracks currently visible columns of the UISplitViewController.
///
/// This set is kept in sync via `UISplitViewControllerDelegate` methods (`willShow` / `willHide`)
/// to reflect which columns are currently rendered in the UI.
/// It ensures that only visible columns are considered (e.g. for accessing topViewController),
/// avoiding crashes when certain columns are collapsed or hidden.
private var visibleColumns: Set<UISplitViewController.Column> = []
///
/// @brief Initializes the SplitView host controller with provided style.
///
/// The style for the SplitView component can be passed only in the initialization method and cannot be changed dynamically.
///
/// @param splitViewHostComponentView The view managed by this controller.
/// @param numberOfColumns Expected number of visible columns.
///
@objc public init(
splitViewHostComponentView: RNSSplitViewHostComponentView,
numberOfColumns: Int
) {
self.splitViewHostComponentView = splitViewHostComponentView
self.splitViewAppearanceCoordinator = RNSSplitViewAppearanceCoordinator()
self.splitViewAppearanceApplicator = RNSSplitViewAppearanceApplicator()
self.fixedColumnsCount = numberOfColumns
super.init(style: RNSSplitViewHostController.styleByNumberOfColumns(numberOfColumns))
delegate = self
}
required init?(coder aDecoder: NSCoder) {
return nil
}
// MARK: Signals
@objc
public func setNeedsUpdateOfChildViewControllers() {
needsChildViewControllersUpdate = true
}
@objc
public func setNeedsAppearanceUpdate() {
splitViewAppearanceCoordinator.needs(.generalUpdate)
}
@objc
public func setNeedsSecondaryScreenNavBarUpdate() {
// We noticed a bug on the pure-native component, which is blocking dynamic updates for showsSecondaryOnlyButton.
// Toggling this flag doesn't refresh the component and is updated after triggerig some other interaction, like changing layout.
// We noticed that we can forcefully refresh navigation bar from UINavigationController level by toggling setNavigationBarHidden.
// After some testing, it looks well and I haven't noticed any flicker - missing button is appearing naturally.
// Please note that this is a hack rather than a solution so feel free to remove this code in case of any problems and treat the bug with toggling button as a platform's issue.
splitViewAppearanceCoordinator.needs(.secondaryScreenNavBarUpdate)
}
@objc
public func setNeedsDisplayModeUpdate() {
splitViewAppearanceCoordinator.needs(.displayModeUpdate)
}
@objc
public func setNeedsOrientationUpdate() {
splitViewAppearanceCoordinator.needs(.orientationUpdate)
}
// MARK: Updating
@objc
public func updateChildViewControllersIfNeeded() {
if needsChildViewControllersUpdate {
updateChildViewControllers()
}
}
///
/// @brief Creates and attaches the SplitView child controllers based on the current React subviews.
///
/// It validates constraints for SplitView hierarchy and it will crash after recognizing an invalid state,
/// e. g. dynamically changed number of columns or number of columns that isn't between defined bounds.
/// If SplitView constraints are met, it attaches SplitViewScreen representatives to SplitViewHost component.
///
@objc
public func updateChildViewControllers() {
precondition(
needsChildViewControllersUpdate,
"[RNScreens] Child view controller must be invalidated when update is forced!")
let currentColumns = filterSubviews(
ofType: RNSSplitViewScreenColumnType.column, in: splitViewReactSubviews)
let currentInspectors = filterSubviews(
ofType: RNSSplitViewScreenColumnType.inspector, in: splitViewReactSubviews)
validateColumns(currentColumns)
validateInspectors(currentInspectors)
let currentViewControllers = currentColumns.map {
RNSSplitViewNavigationController(rootViewController: $0.controller)
}
viewControllers = currentViewControllers
#if compiler(>=6.2)
maybeSetupInspector(currentInspectors)
#endif
for controller in currentViewControllers {
controller.viewFrameOriginChangeObserver = self
}
needsChildViewControllersUpdate = false
}
func updateSplitViewAppearanceIfNeeded() {
splitViewAppearanceApplicator.updateAppearanceIfNeeded(
self.splitViewHostComponentView, self, self.splitViewAppearanceCoordinator)
}
///
/// @brief Triggering appearance updates on secondary column's UINavigationBar component
///
/// It validates that the secondary VC is valid UINavigationController and it updates the navbar
/// state by toggling it's visibility, what should be performed in a single batch of updates.
///
public func refreshSecondaryNavBar() {
let secondaryViewController = viewController(for: .secondary)
assert(
secondaryViewController != nil,
"[RNScreens] Failed to refresh secondary nav bar. Secondary view controller is nil.")
assert(
secondaryViewController is UINavigationController,
"[RNScreens] Expected UINavigationController but got \(type(of: secondaryViewController))")
let navigationController = secondaryViewController as! UINavigationController
/// The assumption is that it should come in a single batch and it won't cause any delays in rendering the content.
navigationController.setNavigationBarHidden(true, animated: false)
navigationController.setNavigationBarHidden(false, animated: false)
}
// MARK: Helpers
///
/// @brief Gets the appropriate style for a specified number of columns.
///
/// This utility maps a given number of columns to the corresponding UISplitViewController.Style.
///
/// @param numberOfColumns The number of columns for the SplitView.
/// @return A UISplitViewController.Style corresponding to the provided column count.
///
static func styleByNumberOfColumns(_ numberOfColumns: Int) -> UISplitViewController.Style {
switch numberOfColumns {
case 2:
return .doubleColumn
case 3:
return .tripleColumn
default:
return .unspecified
}
}
///
/// @brief Filters the given subviews array by a specific column type.
///
/// Iterates over the provided subviews array and returns only the elements that match
/// the specified RNSSplitViewScreenColumnType (e.g., .column, .inspector).
///
/// @param type The target RNSSplitViewScreenColumnType to filter for.
/// @param subviews The array of RNSSplitViewScreenComponentView elements to filter.
/// @return A filtered array of RNSSplitViewScreenComponentView objects with the specified column type.
///
func filterSubviews(
ofType type: RNSSplitViewScreenColumnType, in subviews: [RNSSplitViewScreenComponentView]
) -> [RNSSplitViewScreenComponentView] {
return subviews.filter { $0.columnType == type }
}
// MARK: Public setters
///
/// @brief Shows or hides the inspector screen.
/// @remarks Inspector column is only available for iOS 26 or higher.
///
/// @param showInspector Determines whether the inspector column should be visible.
///
@objc
public func toggleSplitViewInspector(_ showInspector: Bool) {
#if compiler(>=6.2)
if showInspector {
maybeShowInspector()
} else {
maybeHideInspector()
}
#endif
}
// MARK: ReactMountingTransactionObserving
///
/// @brief Called before mounting transaction.
///
@objc
public func reactMountingTransactionWillMount() {
// noop
}
///
/// @brief Called after mounting transaction.
///
/// Updates children and the appearance, checks if the hierarchy is valid after applying updates.
///
@objc
public func reactMountingTransactionDidMount() {
updateChildViewControllersIfNeeded()
updateSplitViewAppearanceIfNeeded()
validateSplitViewHierarchy()
}
// MARK: RNSSplitViewHostOrientationProviding
@objc
public func evaluateOrientation() -> RNSOrientationSwift {
return convertToSwiftEnum(splitViewHostComponentView.orientation)
}
func convertToSwiftEnum(_ orientation: RNSOrientation) -> RNSOrientationSwift {
switch orientation {
case RNSOrientation.inherit:
return .inherit
case RNSOrientation.all:
return .all
case RNSOrientation.allButUpsideDown:
return .allButUpsideDown
case RNSOrientation.portrait:
return .portrait
case RNSOrientation.portraitUp:
return .portraitUp
case RNSOrientation.portraitDown:
return .portraitDown
case RNSOrientation.landscape:
return .landscape
case RNSOrientation.landscapeLeft:
return .landscapeLeft
case RNSOrientation.landscapeRight:
return .landscapeRight
@unknown default:
return .inherit
}
}
// MARK: Validators
///
/// @brief Validates that child structure meets required constraints defined for columns and the inspector.
///
func validateSplitViewHierarchy() {
let columns = filterSubviews(
ofType: RNSSplitViewScreenColumnType.column, in: splitViewReactSubviews)
let inspectors = filterSubviews(
ofType: RNSSplitViewScreenColumnType.inspector, in: splitViewReactSubviews)
validateColumns(columns)
validateInspectors(inspectors)
}
///
/// @brief Ensures that number of columns is valid and hasn't changed dynamically.
///
func validateColumns(_ columns: [RNSSplitViewScreenComponentView]) {
assert(
columns.count >= minNumberOfColumns
&& columns.count <= maxNumberOfColumns,
"[RNScreens] SplitView can only have from \(minNumberOfColumns) to \(maxNumberOfColumns) columns"
)
assert(
columns.count == fixedColumnsCount,
"[RNScreens] SplitView number of columns shouldn't change dynamically")
}
///
/// @brief Ensures that at most one inspector is present.
///
func validateInspectors(_ inspectors: [RNSSplitViewScreenComponentView]) {
assert(
inspectors.count <= maxNumberOfInspectors,
"[RNScreens] SplitView can only have \(maxNumberOfInspectors) inspector")
}
}
extension RNSSplitViewHostController {
///
/// @brief Gets the children RNSSplitViewScreenController instances.
///
/// Accesses SplitView controllers associated with presented columns. It asserts that each view controller is a navigation controller and its topViewController is of type RNSSplitViewScreenController.
///
/// @return An array of RNSSplitViewScreenController corresponding to current split view columns.
///
var splitViewScreenControllers: [RNSSplitViewScreenController] {
return visibleColumns.compactMap { column in
let viewController = self.viewController(for: column)
assert(viewController != nil, "[RNScreens] viewController for column \(column) is nil.")
let splitViewNavigationController = viewController as? RNSSplitViewNavigationController
assert(
splitViewNavigationController != nil,
"[RNScreens] Expected RNSSplitViewNavigationController but got \(type(of: viewController))")
let maybeSplitViewScreenController = splitViewNavigationController?.topViewController
assert(
maybeSplitViewScreenController != nil,
"[RNScreens] RNSSplitViewScreenController is nil for column \(column)")
assert(
maybeSplitViewScreenController is RNSSplitViewScreenController,
"[RNScreens] Expected RNSSplitViewScreenController but got \(type(of: maybeSplitViewScreenController))"
)
return maybeSplitViewScreenController as? RNSSplitViewScreenController
}
}
///
/// @brief Gets all React subviews of type RNSSplitViewScreenComponentView.
///
/// Accesses all the subviews from the reactSubviews collection. It asserts that each one is a RNSSplitViewScreenComponentView.
///
/// @return An array of RNSSplitViewScreenComponentView subviews which are children of the host component view.
///
var splitViewReactSubviews: [RNSSplitViewScreenComponentView] {
return self.splitViewHostComponentView.reactSubviews().lazy.map { subview in
assert(
subview is RNSSplitViewScreenComponentView,
"[RNScreens] Expected RNSSplitViewScreenComponentView but got \(type(of: subview))")
return subview as! RNSSplitViewScreenComponentView
}
}
}
extension RNSSplitViewHostController: RNSSplitViewNavigationControllerViewFrameObserver {
///
/// @brief Notifies that an origin of parent RNSSplitViewNavigationController frame has changed.
///
/// It iterates over children controllers and notifies them for the layout update.
///
/// @param splitViewNavCtrl The navigation controller whose frame origin changed.
///
func splitViewNavCtrlViewDidChangeFrameOrigin(
_ splitViewNavCtrl: RNSSplitViewNavigationController
) {
for controller in self.splitViewScreenControllers {
controller.columnPositioningDidChangeIn(splitViewController: self)
}
}
}
/// This extension is a workaround for missing UISplitViewController symbols introduced in iOS 26,
/// allowing the project to compile and run on iOS 18 or earlier versions.
#if compiler(>=6.2)
extension RNSSplitViewHostController {
///
/// @brief Sets up the inspector column if available.
/// @remarks Inspector columns is available only on iOS 26 or higher.
///
/// Attaches a view controller for the inspector column.
///
/// @param inspectors An array of inspector-type RNSSplitViewScreenComponentView subviews.
///
func maybeSetupInspector(_ inspectors: [RNSSplitViewScreenComponentView]) {
#if !os(tvOS)
if #available(iOS 26.0, *) {
let inspector = inspectors.first
if inspector != nil {
let inspectorViewController = RNSSplitViewNavigationController(
rootViewController: inspector!.controller)
setViewController(inspectorViewController, for: .inspector)
}
}
#endif
}
///
/// @brief Shows the inspector column when available.
/// @remarks Inspector columns is available only on iOS 26 or higher.
///
/// Uses the UISplitViewController's new API introduced in iOS 26 to show the inspector column.
///
func maybeShowInspector() {
#if !os(tvOS)
if #available(iOS 26.0, *) {
show(.inspector)
}
#endif
}
///
/// @brief Hides the inspector column when available.
/// @remarks Inspector columns is available only on iOS 26 or higher.
///
/// Uses the UISplitViewController's new API introduced in iOS 26 to hide the inspector column.
///
func maybeHideInspector() {
#if !os(tvOS)
if #available(iOS 26.0, *) {
hide(.inspector)
}
#endif
}
}
#endif
extension RNSSplitViewHostController: UISplitViewControllerDelegate {
public func splitViewController(
_ svc: UISplitViewController, willShow column: UISplitViewController.Column
) {
visibleColumns.insert(column)
}
public func splitViewController(
_ svc: UISplitViewController, willHide column: UISplitViewController.Column
) {
visibleColumns.remove(column)
}
public func splitViewControllerDidCollapse(_ svc: UISplitViewController) {
reactEventEmitter.emitOnCollapse()
}
public func splitViewControllerDidExpand(_ svc: UISplitViewController) {
reactEventEmitter.emitOnExpand()
}
#if compiler(>=6.2)
///
/// @brief Called after a column in the split view controller has been hidden from the interface.
///
/// Currently emits onHideInspector event for the inspector if applicable.
///
/// @param svc The split view controller that just hid the column.
/// @param column The column that was hidden.
///
public func splitViewController(
_ svc: UISplitViewController, didHide column: UISplitViewController.Column
) {
#if !os(tvOS)
if #available(iOS 26.0, *) {
// TODO: we may consider removing this logic, because it could be handled by onViewDidDisappear on the column level
// On the other hand, maybe dedicated event related to the inspector would be a better approach.
// For now I am leaving it, but feel free to drop this method if there's any reason that `onDidDisappear` works better.
if column != .inspector {
return
}
// `didHide` for modal is called on finger down for dismiss, what is problematic, because we can cancel dismissing modal.
// In this scenario, the modal inspector might receive an invalid state and will deviate from the JS value.
// Therefore, for event emissions, we need to ensure that the view was detached from the view hierarchy, by checking its window.
if let inspectorViewController = viewController(for: .inspector) {
if inspectorViewController.view.window == nil {
reactEventEmitter.emitOnHideInspector()
}
}
}
#endif
}
#endif
@objc
public func splitViewController(
_ svc: UISplitViewController, willChangeTo displayMode: UISplitViewController.DisplayMode
) {
if self.displayMode != displayMode {
reactEventEmitter.emitOnDisplayModeWillChange(from: self.displayMode, to: displayMode)
}
}
}

View File

@@ -0,0 +1,46 @@
import Foundation
import UIKit
/// @brief A protocol that observes origin changes in a RNSSplitViewNavigationControllers view frame.
///
/// The subscriber will be notified when the view's origin changes.
protocol RNSSplitViewNavigationControllerViewFrameObserver: AnyObject {
func splitViewNavCtrlViewDidChangeFrameOrigin(
_ splitViewNavCtrl: RNSSplitViewNavigationController)
}
/// @class RNSSplitViewNavigationController
/// @brief A subclass of UINavigationController, creates a view that wraps view associated with RNSSplitViewScreenController.
///
/// This subclass is responsible for tracking when the underlying view's frame origin changes,
/// allowing for syncing the ShadowTree layout.
///
/// It observes origin changes via key-value observer and notifies a delegate.
@objc
public class RNSSplitViewNavigationController: UINavigationController {
private var viewFrameObservation: NSKeyValueObservation?
weak var viewFrameOriginChangeObserver: RNSSplitViewNavigationControllerViewFrameObserver?
///
/// @brief Called after the view controllers view has been loaded.
///
/// Sets up a frame-origin Key-Value Observer to monitor view position changes and propagate them via delegate to RNSSplitViewHostController.
///
override public func viewDidLoad() {
super.viewDidLoad()
viewFrameObservation?.invalidate()
viewFrameObservation = self.view.observe(\.frame, options: [.old, .new]) {
[weak self] (view, change) in
guard let oldFrame = change.oldValue, let newFrame = change.newValue else { return }
if oldFrame.origin != newFrame.origin {
self?.onViewOriginChange()
}
}
}
private func onViewOriginChange() {
viewFrameOriginChangeObserver?.splitViewNavCtrlViewDidChangeFrameOrigin(self)
}
}

View File

@@ -0,0 +1,69 @@
#pragma once
#import <Foundation/Foundation.h>
// Hide C++ symbols from C compiler used when building Swift module
#if defined(__cplusplus)
#import <react/renderer/components/rnscreens/EventEmitters.h>
namespace react = facebook::react;
#endif // __cplusplus
NS_ASSUME_NONNULL_BEGIN
/**
* @class RNSSplitViewScreenComponentEventEmitter
* @brief Responsible for emitting events from the native SplitViewScreen (column) to the React Element Tree.
*/
@interface RNSSplitViewScreenComponentEventEmitter : NSObject
/**
* @brief Emits the onWillAppear event to notify React Native.
*
* This event is triggered when the SplitView column will be added to the native hierarchy.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnWillAppear;
/**
* @brief Emits the onDidAppear event to notify React Native.
*
* This event is triggered when the SplitView column was added to the native hierarchy.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnDidAppear;
/**
* @brief Emits the onWillDisappear event to notify React Native.
*
* This event is triggered when the SplitView column will be removed from the native hierarchy.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnWillDisappear;
/**
* @brief Emits the onDidDisappear event to notify React Native.
*
* This event is triggered when the SplitView column was removed from the native hierarchy.
*
* @return true if the event was successfully emitted, false otherwise.
*/
- (BOOL)emitOnDidDisappear;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSSplitViewScreenComponentEventEmitter ()
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSSplitViewScreenEventEmitter> &)emitter;
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,58 @@
#import "RNSSplitViewScreenComponentEventEmitter.h"
#import <React/RCTLog.h>
@implementation RNSSplitViewScreenComponentEventEmitter {
std::shared_ptr<const react::RNSSplitViewScreenEventEmitter> _reactEventEmitter;
}
- (BOOL)emitOnWillAppear
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onWillAppear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillAppear event emission due to nullish emitter");
return NO;
}
}
- (BOOL)emitOnDidAppear
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onDidAppear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidAppear event emission due to nullish emitter");
return NO;
}
}
- (BOOL)emitOnWillDisappear
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onWillDisappear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnWillDisappear event emission due to nullish emitter");
return NO;
}
}
- (BOOL)emitOnDidDisappear
{
if (_reactEventEmitter != nullptr) {
_reactEventEmitter->onDidDisappear({});
return YES;
} else {
RCTLogWarn(@"[RNScreens] Skipped OnDidDisappear event emission due to nullish emitter");
return NO;
}
}
- (void)updateEventEmitter:(const std::shared_ptr<const react::RNSSplitViewScreenEventEmitter> &)emitter
{
_reactEventEmitter = emitter;
}
@end

View File

@@ -0,0 +1,80 @@
#pragma once
#import "RNSEnums.h"
#import "RNSReactBaseView.h"
#import "RNSSafeAreaProviding.h"
#import "RNSSplitViewScreenComponentEventEmitter.h"
#import "RNSSplitViewScreenShadowStateProxy.h"
NS_ASSUME_NONNULL_BEGIN
@class RNSSplitViewHostComponentView;
@class RNSSplitViewScreenController;
/**
* @class RNSSplitViewScreenComponentView
* @brief Native view component representing one column in a UISplitViewController layout.
*
* Responsible for a lifecycle management, layout, and event emission for a single screen; used as a child
* of RNSSplitViewHostComponentView.
*/
@interface RNSSplitViewScreenComponentView : RNSReactBaseView <RNSSafeAreaProviding>
@property (nonatomic, strong, readonly, nonnull) RNSSplitViewScreenController *controller;
@property (nonatomic, weak, readwrite, nullable) RNSSplitViewHostComponentView *splitViewHost;
@end
#pragma mark - ShadowTreeState
/**
* @category ShadowTreeState
* @brief Interactions between the host component and the associated ShadowNode.
*/
@interface RNSSplitViewScreenComponentView ()
/**
* @brief Getter for the proxy object that interfaces with the Shadow Tree state for this screen.
*
* The ShadowStateProxy is the object that's responsible for sending layout updates coming from the Host tree to the
* ShadowTree.
*
* @return A pointer to a RNSSplitViewScreenShadowStateProxy instance.
*/
- (nonnull RNSSplitViewScreenShadowStateProxy *)shadowStateProxy;
@end
#pragma mark - Props
/**
* @category Props
* @brief Definitions for React Native props.
*/
@interface RNSSplitViewScreenComponentView ()
/**
* @brief Determines the purpose for the column (classic Column or one of specific types, like Inspector)
*/
@property (nonatomic, readonly) RNSSplitViewScreenColumnType columnType;
@end
#pragma mark - Events
/**
* @category Events
* @brief APIs related to event emission to React Native.
*/
@interface RNSSplitViewScreenComponentView ()
/**
* @brief Getter for the component's event emitter used for emitting events to React.
*
* @return A pointer to RNSSplitViewScreenComponentEventEmitter instance.
*/
- (nonnull RNSSplitViewScreenComponentEventEmitter *)reactEventEmitter;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,204 @@
#import "RNSSplitViewScreenComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTSurfaceTouchHandler.h>
#import <rnscreens/RNSSplitViewScreenComponentDescriptor.h>
#import "RNSConversions.h"
#import "RNSSafeAreaViewNotifications.h"
#import "Swift-Bridging.h"
namespace react = facebook::react;
@implementation RNSSplitViewScreenComponentView {
RNSSplitViewScreenComponentEventEmitter *_Nonnull _reactEventEmitter;
RNSSplitViewScreenController *_Nullable _controller;
RNSSplitViewScreenShadowStateProxy *_Nonnull _shadowStateProxy;
RCTSurfaceTouchHandler *_Nullable _touchHandler;
NSMutableSet<UIView *> *_viewsForFrameCorrection;
}
- (RNSSplitViewScreenController *)controller
{
RCTAssert(
_controller != nil,
@"[RNScreens] Attempt to access RNSSplitViewScreenController before RNSSplitViewScreenComponentView was initialized. (for: %@)",
self);
return _controller;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
[self initState];
}
return self;
}
- (void)initState
{
[self resetProps];
[self setupController];
_reactEventEmitter = [RNSSplitViewScreenComponentEventEmitter new];
_shadowStateProxy = [RNSSplitViewScreenShadowStateProxy new];
_viewsForFrameCorrection = [NSMutableSet set];
}
- (void)setupController
{
_controller = [[RNSSplitViewScreenController alloc] initWithSplitViewScreenComponentView:self];
_controller.view = self;
}
- (void)didMoveToWindow
{
// Starting from iOS 26, a new column type called 'inspector' was introduced.
// This column can be displayed as a modal, independent of the React Native view hierarchy.
// In contrast, prior to iOS 26, all SplitView columns were placed under RCTSurface,
// meaning that touches were handler by RN handlers.
if (@available(iOS 26.0, *)) {
// If the current controllers splitViewController is of type RNSSplitViewHostController,
// we know that we're still inside the RN hierarchy,
// so there's no need to enforce additional touch event support.
if ([_controller isInSplitViewHostSubtree]) {
return;
}
if (self.window != nil) {
if (_touchHandler == nil) {
_touchHandler = [RCTSurfaceTouchHandler new];
}
[_touchHandler attachToView:self];
} else {
[_touchHandler detachFromView:self];
}
}
}
- (void)resetProps
{
static const auto defaultProps = std::make_shared<const react::RNSSplitViewScreenProps>();
_props = defaultProps;
_columnType = RNSSplitViewScreenColumnTypeColumn;
}
- (void)registerForFrameCorrection:(UIView *)view
{
[_viewsForFrameCorrection addObject:view];
}
- (void)unregisterFromFrameCorrection:(UIView *)view
{
[_viewsForFrameCorrection removeObject:view];
}
#pragma mark - Layout
///
/// This override **should be considered as a workaround** for which I made some assumptions:
/// 1. All parents of views with associated `UINavigationController` should have the same width as the SplitView column
/// 2. I'm greedily aligning all native components which are extending `UINavigationController` - is covers both old and
/// new stack implementations, however, it will have an impact on any other native component which will be extending
/// from the same class.
///
- (void)layoutSubviews
{
[super layoutSubviews];
}
#pragma mark - ShadowTreeState
- (nonnull RNSSplitViewScreenShadowStateProxy *)shadowStateProxy
{
RCTAssert(_shadowStateProxy != nil, @"[RNScreens] Attempt to access uninitialized _shadowStateProxy");
return _shadowStateProxy;
}
#pragma mark - Events
- (nonnull RNSSplitViewScreenComponentEventEmitter *)reactEventEmitter
{
RCTAssert(_reactEventEmitter != nil, @"[RNScreens] Attempt to access uninitialized _reactEventEmitter");
return _reactEventEmitter;
}
#pragma mark - RNSSafeAreaProviding
- (UIEdgeInsets)providerSafeAreaInsets
{
return self.safeAreaInsets;
}
- (void)dispatchSafeAreaDidChangeNotification
{
[NSNotificationCenter.defaultCenter postNotificationName:RNSSafeAreaDidChange object:self userInfo:nil];
}
#pragma mark - RNSSafeAreaProviding related methods
// TODO: register for UIKeyboard notifications
- (void)safeAreaInsetsDidChange
{
[super safeAreaInsetsDidChange];
[self dispatchSafeAreaDidChangeNotification];
}
#pragma mark - RCTComponentViewProtocol
+ (react::ComponentDescriptorProvider)componentDescriptorProvider
{
return react::concreteComponentDescriptorProvider<react::RNSSplitViewScreenComponentDescriptor>();
}
+ (BOOL)shouldBeRecycled
{
// There won't be tens of instances of this component usually & it's easier for now.
// We could consider enabling it someday though.
return NO;
}
- (void)updateState:(react::State::Shared const &)state oldState:(react::State::Shared const &)oldState
{
[super updateState:state oldState:oldState];
[_shadowStateProxy updateState:state oldState:oldState];
}
- (void)updateProps:(const facebook::react::Props::Shared &)props
oldProps:(const facebook::react::Props::Shared &)oldProps
{
const auto &oldComponentProps = *std::static_pointer_cast<const react::RNSSplitViewScreenProps>(_props);
const auto &newComponentProps = *std::static_pointer_cast<const react::RNSSplitViewScreenProps>(props);
if (oldComponentProps.columnType != newComponentProps.columnType) {
_columnType = rnscreens::conversion::RNSSplitViewScreenColumnTypeFromScreenProp(newComponentProps.columnType);
}
[super updateProps:props oldProps:oldProps];
}
- (void)updateEventEmitter:(const facebook::react::EventEmitter::Shared &)eventEmitter
{
[super updateEventEmitter:eventEmitter];
[_reactEventEmitter
updateEventEmitter:std::static_pointer_cast<const react::RNSSplitViewScreenEventEmitter>(eventEmitter)];
}
- (void)invalidate
{
// Controller keeps the strong reference to the component via the `.view` property.
// Therefore, we need to enforce a proper cleanup, breaking the retain cycle,
// when we want to destroy the component.
_controller = nil;
}
@end
Class<RCTComponentViewProtocol> RNSSplitViewScreenCls(void)
{
return RNSSplitViewScreenComponentView.class;
}

View File

@@ -0,0 +1,11 @@
#pragma once
#import <React/RCTViewManager.h>
NS_ASSUME_NONNULL_BEGIN
@interface RNSSplitViewScreenComponentViewManager : RCTViewManager
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,7 @@
#import "RNSSplitViewScreenComponentViewManager.h"
@implementation RNSSplitViewScreenComponentViewManager
RCT_EXPORT_MODULE(RNSSplitViewScreenComponentViewManager)
@end

View File

@@ -0,0 +1,133 @@
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()
}
@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
}
// MARK: Signals
@objc
public func setNeedsLifecycleStateUpdate() {
findSplitViewHostController()?.setNeedsUpdateOfChildViewControllers()
}
// MARK: Layout
@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.
///
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"
)
shadowStateProxy.updateShadowState(
ofComponent: splitViewScreenComponentView, inContextOfAncestorView: ancestorView)
}
///
/// @brief Request ShadowNode state update when the SplitView screen frame origin has changed.
///
/// @param splitViewController The UISplitViewController whose layout positioning changed, represented by RNSSplitViewHostController.
///
func columnPositioningDidChangeIn(splitViewController: UISplitViewController) {
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()
}
}

View File

@@ -0,0 +1,84 @@
#pragma once
#import <Foundation/Foundation.h>
#if defined(__cplusplus)
#import <react/renderer/core/State.h>
namespace react = facebook::react;
#endif // __cplusplus
NS_ASSUME_NONNULL_BEGIN
@class RNSSplitViewScreenComponentView;
/**
* @class RNSSplitViewScreenShadowStateProxy
* @brief Manages communication between native UIView layout and associated React Native ShadowNode state.
*
* This proxy enables RNSSplitViewScreenComponentView to propagate visual and layout-level state
* back to the Shadow Tree via RNSSplitViewScreenShadowNode.
*/
@interface RNSSplitViewScreenShadowStateProxy : NSObject
/**
* @brief Triggers a shadow state update for the given SplitViewScreen component.
*
* Internally uses the components frame in UIWindow coordinates to update the Shadow Tree state.
*
* @param screenComponentView An instance of RNSSplitViewScreenComponentView whose state should be updated.
*/
- (void)updateShadowStateOfComponent:(RNSSplitViewScreenComponentView *)screenComponentView;
/**
* @brief Triggers a shadow state update for the given SplitViewScreen component in the context of a given ancestor
* view.
*
* Converts the split view screen's local frame to coordinates of the specified ancestor view
* before applying the update to the Shadow Tree. If the ancestor haven't been defined frame is calculated relatively to
* the UIWindow.
*
* @param screenComponentView An instance of RNSSplitViewScreenComponentView whose state should be updated.
* @param ancestorView An optional UIView in whose coordinate space the frame should be computed.
*/
- (void)updateShadowStateOfComponent:(RNSSplitViewScreenComponentView *)screenComponentView
inContextOfAncestorView:(UIView *_Nullable)ancestorView;
/**
* @brief Send an update to ShadowNode state with given frame if needed.
*
* Converts the frame to coordinates of the specified ancestor view,
* before applying the update to the Shadow Tree.
*
* @param screenComponentView an instance of RNSSplitViewScreenComponentView whose state should be updated,
* @param frame frame to update the shadow state with; it must be in coordinate system of `screenComponentView`,
* @param ancestorView coordinate-system provider view, relative to which the frame should be converted before sending
* the update.
*/
- (void)updateShadowStateOfComponent:(RNSSplitViewScreenComponentView *)screenComponentView
withFrame:(CGRect)frame
inContextOfAncestorView:(nonnull UIView *)ancestorView;
/**
* @brief Send an update to ShadowNode state with given layout metrics.
*
* Updates size and origin in the ShadowNode state, if changed.
*
* @param frame A CGRect defining the component's layout metrics.
*/
- (void)updateShadowStateWithFrame:(CGRect)frame;
@end
#pragma mark - Hidden from Swift
#if defined(__cplusplus)
@interface RNSSplitViewScreenShadowStateProxy ()
- (void)updateState:(react::State::Shared const &)state oldState:(react::State::Shared const &)oldState;
@end
#endif // __cplusplus
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,75 @@
#import "RNSSplitViewScreenShadowStateProxy.h"
#import "RNSSplitViewScreenComponentView.h"
#import <React/RCTAssert.h>
#import <React/RCTConversions.h>
#import <cxxreact/ReactNativeVersion.h>
#import <rnscreens/RNSSplitViewScreenShadowNode.h>
namespace react = facebook::react;
@implementation RNSSplitViewScreenShadowStateProxy {
react::RNSSplitViewScreenShadowNode::ConcreteState::Shared _state;
CGRect _lastScheduledFrame;
}
- (instancetype)init
{
if (self = [super init]) {
_lastScheduledFrame = CGRectNull;
}
return self;
}
- (void)updateShadowStateOfComponent:(RNSSplitViewScreenComponentView *)screenComponentView
{
[self updateShadowStateOfComponent:screenComponentView inContextOfAncestorView:nil];
}
- (void)updateShadowStateOfComponent:(RNSSplitViewScreenComponentView *)screenComponentView
inContextOfAncestorView:(UIView *_Nullable)ancestorView
{
CGRect frame = screenComponentView.frame;
if (ancestorView != nil) {
frame = [screenComponentView convertRect:frame toView:ancestorView];
}
[self updateShadowStateWithFrame:frame];
}
- (void)updateShadowStateWithFrame:(CGRect)frame
{
if (_state == nullptr) {
return;
}
if (!CGRectEqualToRect(frame, _lastScheduledFrame)) {
auto newState = react::RNSSplitViewScreenState{RCTSizeFromCGSize(frame.size), RCTPointFromCGPoint(frame.origin)};
_state->updateState(
std::move(newState)
// TODO: @t0maboro - remove this compilation check once TVOSExample is upgraded to RN 82+
#if REACT_NATIVE_VERSION_MINOR >= 82
,
facebook::react::EventQueue::UpdateMode::unstable_Immediate
#endif
);
_lastScheduledFrame = frame;
}
}
- (void)updateShadowStateOfComponent:(RNSSplitViewScreenComponentView *)screenComponentView
withFrame:(CGRect)frame
inContextOfAncestorView:(nonnull UIView *)ancestorView
{
RCTAssert(ancestorView != nil, @"[RNScreens] ancestorView must not be nil");
CGRect convertedFrame = [screenComponentView convertRect:frame toView:ancestorView];
[self updateShadowStateWithFrame:convertedFrame];
}
- (void)updateState:(react::State::Shared const &)state oldState:(react::State::Shared const &)oldState
{
_state = std::static_pointer_cast<const react::RNSSplitViewScreenShadowNode::ConcreteState>(state);
}
@end