405 lines
15 KiB
Plaintext
405 lines
15 KiB
Plaintext
#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;
|
|
_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)willMoveToWindow:(UIWindow *)newWindow
|
|
{
|
|
if (newWindow == nil) {
|
|
[self invalidate];
|
|
}
|
|
}
|
|
|
|
- (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;
|
|
}
|
|
}
|
|
|
|
- (void)invalidate
|
|
{
|
|
// We assume that split host is removed from view hierarchy **only** when
|
|
// whole component is destroyed & therefore we do the necessary cleanup here.
|
|
// If at some point that statement does not hold anymore, this cleanup
|
|
// should be moved to a different place.
|
|
for (RNSSplitViewScreenComponentView *subview in _reactSubviews) {
|
|
[subview invalidate];
|
|
}
|
|
}
|
|
|
|
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 - RCTViewComponentViewProtocol
|
|
|
|
- (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 (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
|