This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1590 lines
55 KiB
1590 lines
55 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* This source code is licensed under the MIT license found in the |
|
* LICENSE file in the root directory of this source tree. |
|
*/ |
|
|
|
#import "RCTUIManager.h" |
|
|
|
#import <AVFoundation/AVFoundation.h> |
|
|
|
#import "RCTAccessibilityManager.h" |
|
#import "RCTAssert.h" |
|
#import "RCTBridge+Private.h" |
|
#import "RCTBridge.h" |
|
#import "RCTComponent.h" |
|
#import "RCTComponentData.h" |
|
#import "RCTConvert.h" |
|
#import "RCTDefines.h" |
|
#import "RCTEventDispatcher.h" |
|
#import "RCTLayoutAnimation.h" |
|
#import "RCTLayoutAnimationGroup.h" |
|
#import "RCTLog.h" |
|
#import "RCTModuleData.h" |
|
#import "RCTModuleMethod.h" |
|
#import "RCTProfile.h" |
|
#import "RCTRootContentView.h" |
|
#import "RCTRootShadowView.h" |
|
#import "RCTRootViewInternal.h" |
|
#import "RCTScrollableProtocol.h" |
|
#import "RCTShadowView+Internal.h" |
|
#import "RCTShadowView.h" |
|
#import "RCTSurfaceRootShadowView.h" |
|
#import "RCTSurfaceRootView.h" |
|
#import "RCTUIManagerObserverCoordinator.h" |
|
#import "RCTUIManagerUtils.h" |
|
#import "RCTUtils.h" |
|
#import "RCTView.h" |
|
#import "RCTViewManager.h" |
|
#import "UIView+React.h" |
|
|
|
static void RCTTraverseViewNodes(id<RCTComponent> view, void (^block)(id<RCTComponent>)) |
|
{ |
|
if (view.reactTag) { |
|
block(view); |
|
|
|
for (id<RCTComponent> subview in view.reactSubviews) { |
|
RCTTraverseViewNodes(subview, block); |
|
} |
|
} |
|
} |
|
|
|
NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification = @"RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification"; |
|
|
|
@implementation RCTUIManager |
|
{ |
|
// Root views are only mutated on the shadow queue |
|
NSMutableSet<NSNumber *> *_rootViewTags; |
|
NSMutableArray<RCTViewManagerUIBlock> *_pendingUIBlocks; |
|
|
|
// Animation |
|
RCTLayoutAnimationGroup *_layoutAnimationGroup; // Main thread only |
|
|
|
NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only |
|
NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; // Main thread only |
|
|
|
NSMapTable<RCTShadowView *, NSArray<NSString *> *> *_shadowViewsWithUpdatedProps; // UIManager queue only. |
|
NSHashTable<RCTShadowView *> *_shadowViewsWithUpdatedChildren; // UIManager queue only. |
|
|
|
// Keyed by viewName |
|
NSDictionary *_componentDataByName; |
|
} |
|
|
|
@synthesize bridge = _bridge; |
|
|
|
RCT_EXPORT_MODULE() |
|
|
|
+ (BOOL)requiresMainQueueSetup |
|
{ |
|
return NO; |
|
} |
|
|
|
- (void)dealloc |
|
{ |
|
[NSNotificationCenter.defaultCenter removeObserver:self]; |
|
} |
|
|
|
- (void)invalidate |
|
{ |
|
/** |
|
* Called on the JS Thread since all modules are invalidated on the JS thread |
|
*/ |
|
|
|
// This only accessed from the shadow queue |
|
_pendingUIBlocks = nil; |
|
|
|
RCTExecuteOnMainQueue(^{ |
|
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"UIManager invalidate", nil); |
|
for (NSNumber *rootViewTag in self->_rootViewTags) { |
|
UIView *rootView = self->_viewRegistry[rootViewTag]; |
|
if ([rootView conformsToProtocol:@protocol(RCTInvalidating)]) { |
|
[(id<RCTInvalidating>)rootView invalidate]; |
|
} |
|
} |
|
|
|
self->_rootViewTags = nil; |
|
self->_shadowViewRegistry = nil; |
|
self->_viewRegistry = nil; |
|
self->_bridge = nil; |
|
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
|
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); |
|
}); |
|
} |
|
|
|
- (NSMutableDictionary<NSNumber *, RCTShadowView *> *)shadowViewRegistry |
|
{ |
|
// NOTE: this method only exists so that it can be accessed by unit tests |
|
if (!_shadowViewRegistry) { |
|
_shadowViewRegistry = [NSMutableDictionary new]; |
|
} |
|
return _shadowViewRegistry; |
|
} |
|
|
|
- (NSMutableDictionary<NSNumber *, UIView *> *)viewRegistry |
|
{ |
|
// NOTE: this method only exists so that it can be accessed by unit tests |
|
if (!_viewRegistry) { |
|
_viewRegistry = [NSMutableDictionary new]; |
|
} |
|
return _viewRegistry; |
|
} |
|
|
|
- (void)setBridge:(RCTBridge *)bridge |
|
{ |
|
RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance"); |
|
_bridge = bridge; |
|
|
|
_shadowViewRegistry = [NSMutableDictionary new]; |
|
_viewRegistry = [NSMutableDictionary new]; |
|
|
|
_shadowViewsWithUpdatedProps = [NSMapTable weakToStrongObjectsMapTable]; |
|
_shadowViewsWithUpdatedChildren = [NSHashTable weakObjectsHashTable]; |
|
|
|
// Internal resources |
|
_pendingUIBlocks = [NSMutableArray new]; |
|
_rootViewTags = [NSMutableSet new]; |
|
|
|
_observerCoordinator = [RCTUIManagerObserverCoordinator new]; |
|
|
|
// Get view managers from bridge |
|
NSMutableDictionary *componentDataByName = [NSMutableDictionary new]; |
|
for (Class moduleClass in _bridge.moduleClasses) { |
|
if ([moduleClass isSubclassOfClass:[RCTViewManager class]]) { |
|
RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass |
|
bridge:_bridge]; |
|
componentDataByName[componentData.name] = componentData; |
|
} |
|
} |
|
|
|
_componentDataByName = [componentDataByName copy]; |
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self |
|
selector:@selector(didReceiveNewContentSizeMultiplier) |
|
name:RCTAccessibilityManagerDidUpdateMultiplierNotification |
|
object:_bridge.accessibilityManager]; |
|
#if !TARGET_OS_TV |
|
[[NSNotificationCenter defaultCenter] addObserver:self |
|
selector:@selector(namedOrientationDidChange) |
|
name:UIDeviceOrientationDidChangeNotification |
|
object:nil]; |
|
#endif |
|
[RCTLayoutAnimation initializeStatics]; |
|
} |
|
|
|
#pragma mark - Event emitting |
|
|
|
- (void)didReceiveNewContentSizeMultiplier |
|
{ |
|
// Report the event across the bridge. |
|
#pragma clang diagnostic push |
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations" |
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"didUpdateContentSizeMultiplier" |
|
body:@([_bridge.accessibilityManager multiplier])]; |
|
#pragma clang diagnostic pop |
|
|
|
RCTExecuteOnUIManagerQueue(^{ |
|
[[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification |
|
object:self]; |
|
[self setNeedsLayout]; |
|
}); |
|
} |
|
|
|
#if !TARGET_OS_TV |
|
// Names and coordinate system from html5 spec: |
|
// https://developer.mozilla.org/en-US/docs/Web/API/Screen.orientation |
|
// https://developer.mozilla.org/en-US/docs/Web/API/Screen.lockOrientation |
|
static NSDictionary *deviceOrientationEventBody(UIDeviceOrientation orientation) |
|
{ |
|
NSString *name; |
|
NSNumber *degrees = @0; |
|
BOOL isLandscape = NO; |
|
switch(orientation) { |
|
case UIDeviceOrientationPortrait: |
|
name = @"portrait-primary"; |
|
break; |
|
case UIDeviceOrientationPortraitUpsideDown: |
|
name = @"portrait-secondary"; |
|
degrees = @180; |
|
break; |
|
case UIDeviceOrientationLandscapeRight: |
|
name = @"landscape-primary"; |
|
degrees = @-90; |
|
isLandscape = YES; |
|
break; |
|
case UIDeviceOrientationLandscapeLeft: |
|
name = @"landscape-secondary"; |
|
degrees = @90; |
|
isLandscape = YES; |
|
break; |
|
case UIDeviceOrientationFaceDown: |
|
case UIDeviceOrientationFaceUp: |
|
case UIDeviceOrientationUnknown: |
|
// Unsupported |
|
return nil; |
|
} |
|
return @{ |
|
@"name": name, |
|
@"rotationDegrees": degrees, |
|
@"isLandscape": @(isLandscape), |
|
}; |
|
} |
|
|
|
- (void)namedOrientationDidChange |
|
{ |
|
NSDictionary *orientationEvent = deviceOrientationEventBody([UIDevice currentDevice].orientation); |
|
if (!orientationEvent) { |
|
return; |
|
} |
|
|
|
#pragma clang diagnostic push |
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations" |
|
[_bridge.eventDispatcher sendDeviceEventWithName:@"namedOrientationDidChange" |
|
body:orientationEvent]; |
|
#pragma clang diagnostic pop |
|
} |
|
#endif |
|
|
|
- (dispatch_queue_t)methodQueue |
|
{ |
|
return RCTGetUIManagerQueue(); |
|
} |
|
|
|
- (void)registerRootViewTag:(NSNumber *)rootTag |
|
{ |
|
RCTAssertUIManagerQueue(); |
|
|
|
RCTAssert(RCTIsReactRootView(rootTag), |
|
@"Attempt to register rootTag (%@) which is not actually root tag.", rootTag); |
|
|
|
RCTAssert(![_rootViewTags containsObject:rootTag], |
|
@"Attempt to register rootTag (%@) which was already registred.", rootTag); |
|
|
|
[_rootViewTags addObject:rootTag]; |
|
|
|
// Registering root shadow view |
|
RCTSurfaceRootShadowView *shadowView = [RCTSurfaceRootShadowView new]; |
|
shadowView.reactTag = rootTag; |
|
_shadowViewRegistry[rootTag] = shadowView; |
|
|
|
// Registering root view |
|
RCTExecuteOnMainQueue(^{ |
|
RCTSurfaceRootView *rootView = [RCTSurfaceRootView new]; |
|
rootView.reactTag = rootTag; |
|
self->_viewRegistry[rootTag] = rootView; |
|
}); |
|
} |
|
|
|
- (void)registerRootView:(RCTRootContentView *)rootView |
|
{ |
|
RCTAssertMainQueue(); |
|
|
|
NSNumber *reactTag = rootView.reactTag; |
|
RCTAssert(RCTIsReactRootView(reactTag), |
|
@"View %@ with tag #%@ is not a root view", rootView, reactTag); |
|
|
|
UIView *existingView = _viewRegistry[reactTag]; |
|
RCTAssert(existingView == nil || existingView == rootView, |
|
@"Expect all root views to have unique tag. Added %@ twice", reactTag); |
|
|
|
CGSize availableSize = rootView.availableSize; |
|
|
|
// Register view |
|
_viewRegistry[reactTag] = rootView; |
|
|
|
// Register shadow view |
|
RCTExecuteOnUIManagerQueue(^{ |
|
if (!self->_viewRegistry) { |
|
return; |
|
} |
|
|
|
RCTRootShadowView *shadowView = [RCTRootShadowView new]; |
|
shadowView.availableSize = availableSize; |
|
shadowView.reactTag = reactTag; |
|
shadowView.viewName = NSStringFromClass([rootView class]); |
|
self->_shadowViewRegistry[shadowView.reactTag] = shadowView; |
|
[self->_rootViewTags addObject:reactTag]; |
|
}); |
|
} |
|
|
|
- (NSString *)viewNameForReactTag:(NSNumber *)reactTag |
|
{ |
|
RCTAssertUIManagerQueue(); |
|
return _shadowViewRegistry[reactTag].viewName; |
|
} |
|
|
|
- (UIView *)viewForReactTag:(NSNumber *)reactTag |
|
{ |
|
RCTAssertMainQueue(); |
|
return _viewRegistry[reactTag]; |
|
} |
|
|
|
- (RCTShadowView *)shadowViewForReactTag:(NSNumber *)reactTag |
|
{ |
|
RCTAssertUIManagerQueue(); |
|
return _shadowViewRegistry[reactTag]; |
|
} |
|
|
|
- (void)_executeBlockWithShadowView:(void (^)(RCTShadowView *shadowView))block forTag:(NSNumber *)tag |
|
{ |
|
RCTAssertMainQueue(); |
|
|
|
RCTExecuteOnUIManagerQueue(^{ |
|
RCTShadowView *shadowView = self->_shadowViewRegistry[tag]; |
|
|
|
if (shadowView == nil) { |
|
RCTLogInfo(@"Could not locate shadow view with tag #%@, this is probably caused by a temporary inconsistency between native views and shadow views.", tag); |
|
return; |
|
} |
|
|
|
block(shadowView); |
|
}); |
|
} |
|
|
|
- (void)setAvailableSize:(CGSize)availableSize forRootView:(UIView *)rootView |
|
{ |
|
RCTAssertMainQueue(); |
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) { |
|
RCTAssert([shadowView isKindOfClass:[RCTRootShadowView class]], @"Located shadow view is actually not root view."); |
|
|
|
RCTRootShadowView *rootShadowView = (RCTRootShadowView *)shadowView; |
|
|
|
if (CGSizeEqualToSize(availableSize, rootShadowView.availableSize)) { |
|
return; |
|
} |
|
|
|
rootShadowView.availableSize = availableSize; |
|
[self setNeedsLayout]; |
|
} forTag:rootView.reactTag]; |
|
} |
|
|
|
- (void)setLocalData:(NSObject *)localData forView:(UIView *)view |
|
{ |
|
RCTAssertMainQueue(); |
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) { |
|
shadowView.localData = localData; |
|
[self setNeedsLayout]; |
|
} forTag:view.reactTag]; |
|
} |
|
|
|
/** |
|
* TODO(yuwang): implement the nativeID functionality in a more efficient way |
|
* instead of searching the whole view tree |
|
*/ |
|
- (UIView *)viewForNativeID:(NSString *)nativeID withRootTag:(NSNumber *)rootTag |
|
{ |
|
RCTAssertMainQueue(); |
|
UIView *view = [self viewForReactTag:rootTag]; |
|
return [self _lookupViewForNativeID:nativeID inView:view]; |
|
} |
|
|
|
- (UIView *)_lookupViewForNativeID:(NSString *)nativeID inView:(UIView *)view |
|
{ |
|
RCTAssertMainQueue(); |
|
if (view != nil && [nativeID isEqualToString:view.nativeID]) { |
|
return view; |
|
} |
|
|
|
for (UIView *subview in view.subviews) { |
|
UIView *targetView = [self _lookupViewForNativeID:nativeID inView:subview]; |
|
if (targetView != nil) { |
|
return targetView; |
|
} |
|
} |
|
return nil; |
|
} |
|
|
|
- (void)setSize:(CGSize)size forView:(UIView *)view |
|
{ |
|
RCTAssertMainQueue(); |
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) { |
|
if (CGSizeEqualToSize(size, shadowView.size)) { |
|
return; |
|
} |
|
|
|
shadowView.size = size; |
|
[self setNeedsLayout]; |
|
} forTag:view.reactTag]; |
|
} |
|
|
|
- (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize forView:(UIView *)view |
|
{ |
|
RCTAssertMainQueue(); |
|
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) { |
|
if (CGSizeEqualToSize(shadowView.intrinsicContentSize, intrinsicContentSize)) { |
|
return; |
|
} |
|
|
|
shadowView.intrinsicContentSize = intrinsicContentSize; |
|
[self setNeedsLayout]; |
|
} forTag:view.reactTag]; |
|
} |
|
|
|
/** |
|
* Unregisters views from registries |
|
*/ |
|
- (void)_purgeChildren:(NSArray<id<RCTComponent>> *)children |
|
fromRegistry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)registry |
|
{ |
|
for (id<RCTComponent> child in children) { |
|
RCTTraverseViewNodes(registry[child.reactTag], ^(id<RCTComponent> subview) { |
|
RCTAssert(![subview isReactRootView], @"Root views should not be unregistered"); |
|
if ([subview conformsToProtocol:@protocol(RCTInvalidating)]) { |
|
[(id<RCTInvalidating>)subview invalidate]; |
|
} |
|
[registry removeObjectForKey:subview.reactTag]; |
|
}); |
|
} |
|
} |
|
|
|
- (void)addUIBlock:(RCTViewManagerUIBlock)block |
|
{ |
|
RCTAssertUIManagerQueue(); |
|
|
|
if (!block || !_viewRegistry) { |
|
return; |
|
} |
|
|
|
[_pendingUIBlocks addObject:block]; |
|
} |
|
|
|
- (void)prependUIBlock:(RCTViewManagerUIBlock)block |
|
{ |
|
RCTAssertUIManagerQueue(); |
|
|
|
if (!block || !_viewRegistry) { |
|
return; |
|
} |
|
|
|
[_pendingUIBlocks insertObject:block atIndex:0]; |
|
} |
|
|
|
- (void)setNextLayoutAnimationGroup:(RCTLayoutAnimationGroup *)layoutAnimationGroup |
|
{ |
|
RCTAssertMainQueue(); |
|
|
|
if (_layoutAnimationGroup && ![_layoutAnimationGroup isEqual:layoutAnimationGroup]) { |
|
RCTLogWarn(@"Warning: Overriding previous layout animation with new one before the first began:\n%@ -> %@.", |
|
[_layoutAnimationGroup description], |
|
[layoutAnimationGroup description]); |
|
} |
|
|
|
_layoutAnimationGroup = layoutAnimationGroup; |
|
} |
|
|
|
- (RCTViewManagerUIBlock)uiBlockWithLayoutUpdateForRootView:(RCTRootShadowView *)rootShadowView |
|
{ |
|
RCTAssertUIManagerQueue(); |
|
|
|
NSHashTable<RCTShadowView *> *affectedShadowViews = [NSHashTable weakObjectsHashTable]; |
|
[rootShadowView layoutWithAffectedShadowViews:affectedShadowViews]; |
|
|
|
if (!affectedShadowViews.count) { |
|
// no frame change results in no UI update block |
|
return nil; |
|
} |
|
|
|
typedef struct { |
|
CGRect frame; |
|
UIUserInterfaceLayoutDirection layoutDirection; |
|
BOOL isNew; |
|
BOOL parentIsNew; |
|
} RCTFrameData; |
|
|
|
// Construct arrays then hand off to main thread |
|
NSUInteger count = affectedShadowViews.count; |
|
NSMutableArray *reactTags = [[NSMutableArray alloc] initWithCapacity:count]; |
|
NSMutableData *framesData = [[NSMutableData alloc] initWithLength:sizeof(RCTFrameData) * count]; |
|
{ |
|
NSUInteger index = 0; |
|
RCTFrameData *frameDataArray = (RCTFrameData *)framesData.mutableBytes; |
|
for (RCTShadowView *shadowView in affectedShadowViews) { |
|
reactTags[index] = shadowView.reactTag; |
|
RCTLayoutMetrics layoutMetrics = shadowView.layoutMetrics; |
|
frameDataArray[index++] = (RCTFrameData){ |
|
layoutMetrics.frame, |
|
layoutMetrics.layoutDirection, |
|
shadowView.isNewView, |
|
shadowView.superview.isNewView, |
|
}; |
|
} |
|
} |
|
|
|
for (RCTShadowView *shadowView in affectedShadowViews) { |
|
|
|
// We have to do this after we build the parentsAreNew array. |
|
shadowView.newView = NO; |
|
|
|
NSNumber *reactTag = shadowView.reactTag; |
|
|
|
if (shadowView.onLayout) { |
|
CGRect frame = shadowView.layoutMetrics.frame; |
|
shadowView.onLayout(@{ |
|
@"layout": @{ |
|
@"x": @(frame.origin.x), |
|
@"y": @(frame.origin.y), |
|
@"width": @(frame.size.width), |
|
@"height": @(frame.size.height), |
|
}, |
|
}); |
|
} |
|
|
|
if ( |
|
RCTIsReactRootView(reactTag) && |
|
[shadowView isKindOfClass:[RCTRootShadowView class]] |
|
) { |
|
CGSize contentSize = shadowView.layoutMetrics.frame.size; |
|
|
|
RCTExecuteOnMainQueue(^{ |
|
UIView *view = self->_viewRegistry[reactTag]; |
|
RCTAssert(view != nil, @"view (for ID %@) not found", reactTag); |
|
|
|
RCTRootView *rootView = (RCTRootView *)[view superview]; |
|
if ([rootView isKindOfClass:[RCTRootView class]]) { |
|
rootView.intrinsicContentSize = contentSize; |
|
} |
|
}); |
|
} |
|
} |
|
|
|
// Perform layout (possibly animated) |
|
return ^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
|
|
const RCTFrameData *frameDataArray = (const RCTFrameData *)framesData.bytes; |
|
RCTLayoutAnimationGroup *layoutAnimationGroup = uiManager->_layoutAnimationGroup; |
|
|
|
__block NSUInteger completionsCalled = 0; |
|
|
|
NSInteger index = 0; |
|
for (NSNumber *reactTag in reactTags) { |
|
RCTFrameData frameData = frameDataArray[index++]; |
|
|
|
UIView *view = viewRegistry[reactTag]; |
|
CGRect frame = frameData.frame; |
|
|
|
UIUserInterfaceLayoutDirection layoutDirection = frameData.layoutDirection; |
|
BOOL isNew = frameData.isNew; |
|
RCTLayoutAnimation *updatingLayoutAnimation = isNew ? nil : layoutAnimationGroup.updatingLayoutAnimation; |
|
BOOL shouldAnimateCreation = isNew && !frameData.parentIsNew; |
|
RCTLayoutAnimation *creatingLayoutAnimation = shouldAnimateCreation ? layoutAnimationGroup.creatingLayoutAnimation : nil; |
|
|
|
void (^completion)(BOOL) = ^(BOOL finished) { |
|
completionsCalled++; |
|
if (layoutAnimationGroup.callback && completionsCalled == count) { |
|
layoutAnimationGroup.callback(@[@(finished)]); |
|
|
|
// It's unsafe to call this callback more than once, so we nil it out here |
|
// to make sure that doesn't happen. |
|
layoutAnimationGroup.callback = nil; |
|
} |
|
}; |
|
|
|
if (view.reactLayoutDirection != layoutDirection) { |
|
view.reactLayoutDirection = layoutDirection; |
|
} |
|
|
|
if (creatingLayoutAnimation) { |
|
|
|
// Animate view creation |
|
[view reactSetFrame:frame]; |
|
|
|
CATransform3D finalTransform = view.layer.transform; |
|
CGFloat finalOpacity = view.layer.opacity; |
|
|
|
NSString *property = creatingLayoutAnimation.property; |
|
if ([property isEqualToString:@"scaleXY"]) { |
|
view.layer.transform = CATransform3DMakeScale(0, 0, 0); |
|
} else if ([property isEqualToString:@"opacity"]) { |
|
view.layer.opacity = 0.0; |
|
} else { |
|
RCTLogError(@"Unsupported layout animation createConfig property %@", |
|
creatingLayoutAnimation.property); |
|
} |
|
|
|
[creatingLayoutAnimation performAnimations:^{ |
|
if ([property isEqualToString:@"scaleXY"]) { |
|
view.layer.transform = finalTransform; |
|
} else if ([property isEqualToString:@"opacity"]) { |
|
view.layer.opacity = finalOpacity; |
|
} |
|
} withCompletionBlock:completion]; |
|
|
|
} else if (updatingLayoutAnimation) { |
|
|
|
// Animate view update |
|
[updatingLayoutAnimation performAnimations:^{ |
|
[view reactSetFrame:frame]; |
|
} withCompletionBlock:completion]; |
|
|
|
} else { |
|
|
|
// Update without animation |
|
[view reactSetFrame:frame]; |
|
completion(YES); |
|
} |
|
} |
|
|
|
// Clean up |
|
uiManager->_layoutAnimationGroup = nil; |
|
}; |
|
} |
|
|
|
/** |
|
* A method to be called from JS, which takes a container ID and then releases |
|
* all subviews for that container upon receipt. |
|
*/ |
|
RCT_EXPORT_METHOD(removeSubviewsFromContainerWithID:(nonnull NSNumber *)containerID) |
|
{ |
|
id<RCTComponent> container = _shadowViewRegistry[containerID]; |
|
RCTAssert(container != nil, @"container view (for ID %@) not found", containerID); |
|
|
|
NSUInteger subviewsCount = [container reactSubviews].count; |
|
NSMutableArray<NSNumber *> *indices = [[NSMutableArray alloc] initWithCapacity:subviewsCount]; |
|
for (NSUInteger childIndex = 0; childIndex < subviewsCount; childIndex++) { |
|
[indices addObject:@(childIndex)]; |
|
} |
|
|
|
[self manageChildren:containerID |
|
moveFromIndices:nil |
|
moveToIndices:nil |
|
addChildReactTags:nil |
|
addAtIndices:nil |
|
removeAtIndices:indices]; |
|
} |
|
|
|
/** |
|
* Disassociates children from container. Doesn't remove from registries. |
|
* TODO: use [NSArray getObjects:buffer] to reuse same fast buffer each time. |
|
* |
|
* @returns Array of removed items. |
|
*/ |
|
- (NSArray<id<RCTComponent>> *)_childrenToRemoveFromContainer:(id<RCTComponent>)container |
|
atIndices:(NSArray<NSNumber *> *)atIndices |
|
{ |
|
// If there are no indices to move or the container has no subviews don't bother |
|
// We support parents with nil subviews so long as they're all nil so this allows for this behavior |
|
if (atIndices.count == 0 || [container reactSubviews].count == 0) { |
|
return nil; |
|
} |
|
// Construction of removed children must be done "up front", before indices are disturbed by removals. |
|
NSMutableArray<id<RCTComponent>> *removedChildren = [NSMutableArray arrayWithCapacity:atIndices.count]; |
|
RCTAssert(container != nil, @"container view (for ID %@) not found", container); |
|
for (NSNumber *indexNumber in atIndices) { |
|
NSUInteger index = indexNumber.unsignedIntegerValue; |
|
if (index < [container reactSubviews].count) { |
|
[removedChildren addObject:[container reactSubviews][index]]; |
|
} |
|
} |
|
if (removedChildren.count != atIndices.count) { |
|
NSString *message = [NSString stringWithFormat:@"removedChildren count (%tu) was not what we expected (%tu)", |
|
removedChildren.count, atIndices.count]; |
|
RCTFatal(RCTErrorWithMessage(message)); |
|
} |
|
return removedChildren; |
|
} |
|
|
|
- (void)_removeChildren:(NSArray<id<RCTComponent>> *)children |
|
fromContainer:(id<RCTComponent>)container |
|
{ |
|
for (id<RCTComponent> removedChild in children) { |
|
[container removeReactSubview:removedChild]; |
|
} |
|
} |
|
|
|
/** |
|
* Remove subviews from their parent with an animation. |
|
*/ |
|
- (void)_removeChildren:(NSArray<UIView *> *)children |
|
fromContainer:(UIView *)container |
|
withAnimation:(RCTLayoutAnimationGroup *)animation |
|
{ |
|
RCTAssertMainQueue(); |
|
RCTLayoutAnimation *deletingLayoutAnimation = animation.deletingLayoutAnimation; |
|
|
|
__block NSUInteger completionsCalled = 0; |
|
for (UIView *removedChild in children) { |
|
|
|
void (^completion)(BOOL) = ^(BOOL finished) { |
|
completionsCalled++; |
|
|
|
[removedChild removeFromSuperview]; |
|
|
|
if (animation.callback && completionsCalled == children.count) { |
|
animation.callback(@[@(finished)]); |
|
|
|
// It's unsafe to call this callback more than once, so we nil it out here |
|
// to make sure that doesn't happen. |
|
animation.callback = nil; |
|
} |
|
}; |
|
|
|
// Hack: At this moment we have two contradict intents. |
|
// First one: We want to delete the view from view hierarchy. |
|
// Second one: We want to animate this view, which implies the existence of this view in the hierarchy. |
|
// So, we have to remove this view from React's view hierarchy but postpone removing from UIKit's hierarchy. |
|
// Here the problem: the default implementation of `-[UIView removeReactSubview:]` also removes the view from UIKit's hierarchy. |
|
// So, let's temporary restore the view back after removing. |
|
// To do so, we have to memorize original `superview` (which can differ from `container`) and an index of removed view. |
|
UIView *originalSuperview = removedChild.superview; |
|
NSUInteger originalIndex = [originalSuperview.subviews indexOfObjectIdenticalTo:removedChild]; |
|
[container removeReactSubview:removedChild]; |
|
// Disable user interaction while the view is animating |
|
// since the view is (conceptually) deleted and not supposed to be interactive. |
|
removedChild.userInteractionEnabled = NO; |
|
[originalSuperview insertSubview:removedChild atIndex:originalIndex]; |
|
|
|
NSString *property = deletingLayoutAnimation.property; |
|
[deletingLayoutAnimation performAnimations:^{ |
|
if ([property isEqualToString:@"scaleXY"]) { |
|
removedChild.layer.transform = CATransform3DMakeScale(0.001, 0.001, 0.001); |
|
} else if ([property isEqualToString:@"opacity"]) { |
|
removedChild.layer.opacity = 0.0; |
|
} else { |
|
RCTLogError(@"Unsupported layout animation createConfig property %@", |
|
deletingLayoutAnimation.property); |
|
} |
|
} withCompletionBlock:completion]; |
|
} |
|
} |
|
|
|
|
|
RCT_EXPORT_METHOD(removeRootView:(nonnull NSNumber *)rootReactTag) |
|
{ |
|
RCTShadowView *rootShadowView = _shadowViewRegistry[rootReactTag]; |
|
RCTAssert(rootShadowView.superview == nil, @"root view cannot have superview (ID %@)", rootReactTag); |
|
[self _purgeChildren:(NSArray<id<RCTComponent>> *)rootShadowView.reactSubviews |
|
fromRegistry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry]; |
|
[_shadowViewRegistry removeObjectForKey:rootReactTag]; |
|
[_rootViewTags removeObject:rootReactTag]; |
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){ |
|
RCTAssertMainQueue(); |
|
UIView *rootView = viewRegistry[rootReactTag]; |
|
[uiManager _purgeChildren:(NSArray<id<RCTComponent>> *)rootView.reactSubviews |
|
fromRegistry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry]; |
|
[(NSMutableDictionary *)viewRegistry removeObjectForKey:rootReactTag]; |
|
}]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(replaceExistingNonRootView:(nonnull NSNumber *)reactTag |
|
withView:(nonnull NSNumber *)newReactTag) |
|
{ |
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; |
|
RCTAssert(shadowView != nil, @"shadowView (for ID %@) not found", reactTag); |
|
|
|
RCTShadowView *superShadowView = shadowView.superview; |
|
if (!superShadowView) { |
|
RCTAssert(NO, @"shadowView super (of ID %@) not found", reactTag); |
|
return; |
|
} |
|
|
|
NSUInteger indexOfView = [superShadowView.reactSubviews indexOfObjectIdenticalTo:shadowView]; |
|
RCTAssert(indexOfView != NSNotFound, @"View's superview doesn't claim it as subview (id %@)", reactTag); |
|
NSArray<NSNumber *> *removeAtIndices = @[@(indexOfView)]; |
|
NSArray<NSNumber *> *addTags = @[newReactTag]; |
|
[self manageChildren:superShadowView.reactTag |
|
moveFromIndices:nil |
|
moveToIndices:nil |
|
addChildReactTags:addTags |
|
addAtIndices:removeAtIndices |
|
removeAtIndices:removeAtIndices]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(setChildren:(nonnull NSNumber *)containerTag |
|
reactTags:(NSArray<NSNumber *> *)reactTags) |
|
{ |
|
RCTSetChildren(containerTag, reactTags, |
|
(NSDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry); |
|
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){ |
|
|
|
RCTSetChildren(containerTag, reactTags, |
|
(NSDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry); |
|
}]; |
|
|
|
[self _shadowViewDidReceiveUpdatedChildren:_shadowViewRegistry[containerTag]]; |
|
} |
|
|
|
static void RCTSetChildren(NSNumber *containerTag, |
|
NSArray<NSNumber *> *reactTags, |
|
NSDictionary<NSNumber *, id<RCTComponent>> *registry) |
|
{ |
|
id<RCTComponent> container = registry[containerTag]; |
|
NSInteger index = 0; |
|
for (NSNumber *reactTag in reactTags) { |
|
id<RCTComponent> view = registry[reactTag]; |
|
if (view) { |
|
[container insertReactSubview:view atIndex:index++]; |
|
} |
|
} |
|
} |
|
|
|
RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag |
|
moveFromIndices:(NSArray<NSNumber *> *)moveFromIndices |
|
moveToIndices:(NSArray<NSNumber *> *)moveToIndices |
|
addChildReactTags:(NSArray<NSNumber *> *)addChildReactTags |
|
addAtIndices:(NSArray<NSNumber *> *)addAtIndices |
|
removeAtIndices:(NSArray<NSNumber *> *)removeAtIndices) |
|
{ |
|
[self _manageChildren:containerTag |
|
moveFromIndices:moveFromIndices |
|
moveToIndices:moveToIndices |
|
addChildReactTags:addChildReactTags |
|
addAtIndices:addAtIndices |
|
removeAtIndices:removeAtIndices |
|
registry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)_shadowViewRegistry]; |
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){ |
|
[uiManager _manageChildren:containerTag |
|
moveFromIndices:moveFromIndices |
|
moveToIndices:moveToIndices |
|
addChildReactTags:addChildReactTags |
|
addAtIndices:addAtIndices |
|
removeAtIndices:removeAtIndices |
|
registry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)viewRegistry]; |
|
}]; |
|
|
|
[self _shadowViewDidReceiveUpdatedChildren:_shadowViewRegistry[containerTag]]; |
|
} |
|
|
|
- (void)_manageChildren:(NSNumber *)containerTag |
|
moveFromIndices:(NSArray<NSNumber *> *)moveFromIndices |
|
moveToIndices:(NSArray<NSNumber *> *)moveToIndices |
|
addChildReactTags:(NSArray<NSNumber *> *)addChildReactTags |
|
addAtIndices:(NSArray<NSNumber *> *)addAtIndices |
|
removeAtIndices:(NSArray<NSNumber *> *)removeAtIndices |
|
registry:(NSMutableDictionary<NSNumber *, id<RCTComponent>> *)registry |
|
{ |
|
id<RCTComponent> container = registry[containerTag]; |
|
RCTAssert(moveFromIndices.count == moveToIndices.count, @"moveFromIndices had size %tu, moveToIndices had size %tu", moveFromIndices.count, moveToIndices.count); |
|
RCTAssert(addChildReactTags.count == addAtIndices.count, @"there should be at least one React child to add"); |
|
|
|
// Removes (both permanent and temporary moves) are using "before" indices |
|
NSArray<id<RCTComponent>> *permanentlyRemovedChildren = |
|
[self _childrenToRemoveFromContainer:container atIndices:removeAtIndices]; |
|
NSArray<id<RCTComponent>> *temporarilyRemovedChildren = |
|
[self _childrenToRemoveFromContainer:container atIndices:moveFromIndices]; |
|
|
|
BOOL isUIViewRegistry = ((id)registry == (id)_viewRegistry); |
|
if (isUIViewRegistry && _layoutAnimationGroup.deletingLayoutAnimation) { |
|
[self _removeChildren:(NSArray<UIView *> *)permanentlyRemovedChildren |
|
fromContainer:(UIView *)container |
|
withAnimation:_layoutAnimationGroup]; |
|
} else { |
|
[self _removeChildren:permanentlyRemovedChildren fromContainer:container]; |
|
} |
|
|
|
[self _removeChildren:temporarilyRemovedChildren fromContainer:container]; |
|
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry]; |
|
|
|
// Figure out what to insert - merge temporary inserts and adds |
|
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary]; |
|
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) { |
|
destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index]; |
|
} |
|
|
|
for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) { |
|
id<RCTComponent> view = registry[addChildReactTags[index]]; |
|
if (view) { |
|
destinationsToChildrenToAdd[addAtIndices[index]] = view; |
|
} |
|
} |
|
|
|
NSArray<NSNumber *> *sortedIndices = |
|
[destinationsToChildrenToAdd.allKeys sortedArrayUsingSelector:@selector(compare:)]; |
|
for (NSNumber *reactIndex in sortedIndices) { |
|
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex] |
|
atIndex:reactIndex.integerValue]; |
|
} |
|
} |
|
|
|
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag |
|
viewName:(NSString *)viewName |
|
rootTag:(nonnull NSNumber *)rootTag |
|
props:(NSDictionary *)props) |
|
{ |
|
RCTComponentData *componentData = _componentDataByName[viewName]; |
|
if (componentData == nil) { |
|
RCTLogError(@"No component found for view with name \"%@\"", viewName); |
|
} |
|
|
|
// Register shadow view |
|
RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag]; |
|
if (shadowView) { |
|
[componentData setProps:props forShadowView:shadowView]; |
|
_shadowViewRegistry[reactTag] = shadowView; |
|
RCTShadowView *rootView = _shadowViewRegistry[rootTag]; |
|
RCTAssert([rootView isKindOfClass:[RCTRootShadowView class]] || |
|
[rootView isKindOfClass:[RCTSurfaceRootShadowView class]], |
|
@"Given `rootTag` (%@) does not correspond to a valid root shadow view instance.", rootTag); |
|
shadowView.rootView = (RCTRootShadowView *)rootView; |
|
} |
|
|
|
// Dispatch view creation directly to the main thread instead of adding to |
|
// UIBlocks array. This way, it doesn't get deferred until after layout. |
|
__block UIView *preliminaryCreatedView = nil; |
|
|
|
void (^createViewBlock)(void) = ^{ |
|
// Do nothing on the second run. |
|
if (preliminaryCreatedView) { |
|
return; |
|
} |
|
|
|
preliminaryCreatedView = [componentData createViewWithTag:reactTag]; |
|
|
|
if (preliminaryCreatedView) { |
|
self->_viewRegistry[reactTag] = preliminaryCreatedView; |
|
} |
|
}; |
|
|
|
// We cannot guarantee that asynchronously scheduled block will be executed |
|
// *before* a block is added to the regular mounting process (simply because |
|
// mounting process can be managed externally while the main queue is |
|
// locked). |
|
// So, we positively dispatch it asynchronously and double check inside |
|
// the regular mounting block. |
|
|
|
RCTExecuteOnMainQueue(createViewBlock); |
|
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
createViewBlock(); |
|
|
|
if (preliminaryCreatedView) { |
|
[componentData setProps:props forView:preliminaryCreatedView]; |
|
} |
|
}]; |
|
|
|
[self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(updateView:(nonnull NSNumber *)reactTag |
|
viewName:(NSString *)viewName // not always reliable, use shadowView.viewName if available |
|
props:(NSDictionary *)props) |
|
{ |
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; |
|
RCTComponentData *componentData = _componentDataByName[shadowView.viewName ?: viewName]; |
|
[componentData setProps:props forShadowView:shadowView]; |
|
|
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
UIView *view = viewRegistry[reactTag]; |
|
[componentData setProps:props forView:view]; |
|
}]; |
|
|
|
[self _shadowView:shadowView didReceiveUpdatedProps:[props allKeys]]; |
|
} |
|
|
|
- (void)synchronouslyUpdateViewOnUIThread:(NSNumber *)reactTag |
|
viewName:(NSString *)viewName |
|
props:(NSDictionary *)props |
|
{ |
|
RCTAssertMainQueue(); |
|
RCTComponentData *componentData = _componentDataByName[viewName]; |
|
UIView *view = _viewRegistry[reactTag]; |
|
[componentData setProps:props forView:view]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(focus:(nonnull NSNumber *)reactTag) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
UIView *newResponder = viewRegistry[reactTag]; |
|
[newResponder reactFocus]; |
|
}]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(blur:(nonnull NSNumber *)reactTag) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry){ |
|
UIView *currentResponder = viewRegistry[reactTag]; |
|
[currentResponder reactBlur]; |
|
}]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(findSubviewIn:(nonnull NSNumber *)reactTag atPoint:(CGPoint)point callback:(RCTResponseSenderBlock)callback) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
UIView *view = viewRegistry[reactTag]; |
|
UIView *target = [view hitTest:point withEvent:nil]; |
|
CGRect frame = [target convertRect:target.bounds toView:view]; |
|
|
|
while (target.reactTag == nil && target.superview != nil) { |
|
target = target.superview; |
|
} |
|
|
|
callback(@[ |
|
RCTNullIfNil(target.reactTag), |
|
@(frame.origin.x), |
|
@(frame.origin.y), |
|
@(frame.size.width), |
|
@(frame.size.height), |
|
]); |
|
}]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(dispatchViewManagerCommand:(nonnull NSNumber *)reactTag |
|
commandID:(NSInteger)commandID |
|
commandArgs:(NSArray<id> *)commandArgs) |
|
{ |
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; |
|
RCTComponentData *componentData = _componentDataByName[shadowView.viewName]; |
|
Class managerClass = componentData.managerClass; |
|
RCTModuleData *moduleData = [_bridge moduleDataForName:RCTBridgeModuleNameForClass(managerClass)]; |
|
id<RCTBridgeMethod> method = moduleData.methods[commandID]; |
|
|
|
NSArray *args = [@[reactTag] arrayByAddingObjectsFromArray:commandArgs]; |
|
[method invokeWithBridge:_bridge module:componentData.manager arguments:args]; |
|
} |
|
|
|
- (void)batchDidComplete |
|
{ |
|
[self _layoutAndMount]; |
|
} |
|
|
|
/** |
|
* Sets up animations, computes layout, creates UI mounting blocks for computed layout, |
|
* runs these blocks and all other already existing blocks. |
|
*/ |
|
- (void)_layoutAndMount |
|
{ |
|
[self _dispatchPropsDidChangeEvents]; |
|
[self _dispatchChildrenDidChangeEvents]; |
|
|
|
[_observerCoordinator uiManagerWillPerformLayout:self]; |
|
|
|
// Perform layout |
|
for (NSNumber *reactTag in _rootViewTags) { |
|
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag]; |
|
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]]; |
|
} |
|
|
|
[_observerCoordinator uiManagerDidPerformLayout:self]; |
|
|
|
[_observerCoordinator uiManagerWillPerformMounting:self]; |
|
|
|
[self flushUIBlocksWithCompletion:^{ |
|
[self->_observerCoordinator uiManagerDidPerformMounting:self]; |
|
}]; |
|
} |
|
|
|
- (void)flushUIBlocksWithCompletion:(void (^)(void))completion; |
|
{ |
|
RCTAssertUIManagerQueue(); |
|
|
|
// First copy the previous blocks into a temporary variable, then reset the |
|
// pending blocks to a new array. This guards against mutation while |
|
// processing the pending blocks in another thread. |
|
NSArray<RCTViewManagerUIBlock> *previousPendingUIBlocks = _pendingUIBlocks; |
|
_pendingUIBlocks = [NSMutableArray new]; |
|
|
|
if (previousPendingUIBlocks.count == 0) { |
|
completion(); |
|
return; |
|
} |
|
|
|
__weak typeof(self) weakSelf = self; |
|
|
|
void (^mountingBlock)(void) = ^{ |
|
typeof(self) strongSelf = weakSelf; |
|
|
|
@try { |
|
for (RCTViewManagerUIBlock block in previousPendingUIBlocks) { |
|
block(strongSelf, strongSelf->_viewRegistry); |
|
} |
|
} |
|
@catch (NSException *exception) { |
|
RCTLogError(@"Exception thrown while executing UI block: %@", exception); |
|
} |
|
}; |
|
|
|
if ([self.observerCoordinator uiManager:self performMountingWithBlock:mountingBlock]) { |
|
completion(); |
|
return; |
|
} |
|
|
|
// Execute the previously queued UI blocks |
|
RCTProfileBeginFlowEvent(); |
|
RCTExecuteOnMainQueue(^{ |
|
RCTProfileEndFlowEvent(); |
|
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[UIManager flushUIBlocks]", (@{ |
|
@"count": [@(previousPendingUIBlocks.count) stringValue], |
|
})); |
|
|
|
mountingBlock(); |
|
|
|
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @""); |
|
|
|
RCTExecuteOnUIManagerQueue(completion); |
|
}); |
|
} |
|
|
|
- (void)setNeedsLayout |
|
{ |
|
// If there is an active batch layout will happen when batch finished, so we will wait for that. |
|
// Otherwise we immediately trigger layout. |
|
if (![_bridge isBatchActive] && ![_bridge isLoading]) { |
|
[self _layoutAndMount]; |
|
} |
|
} |
|
|
|
- (void)_shadowView:(RCTShadowView *)shadowView didReceiveUpdatedProps:(NSArray<NSString *> *)props |
|
{ |
|
// We collect a set with changed `shadowViews` and its changed props, |
|
// so we have to maintain this collection properly. |
|
NSArray<NSString *> *previousProps; |
|
if ((previousProps = [_shadowViewsWithUpdatedProps objectForKey:shadowView])) { |
|
// Merging already registred changed props and new ones. |
|
NSMutableSet *set = [NSMutableSet setWithArray:previousProps]; |
|
[set addObjectsFromArray:props]; |
|
props = [set allObjects]; |
|
} |
|
|
|
[_shadowViewsWithUpdatedProps setObject:props forKey:shadowView]; |
|
} |
|
|
|
- (void)_shadowViewDidReceiveUpdatedChildren:(RCTShadowView *)shadowView |
|
{ |
|
[_shadowViewsWithUpdatedChildren addObject:shadowView]; |
|
} |
|
|
|
- (void)_dispatchChildrenDidChangeEvents |
|
{ |
|
if (_shadowViewsWithUpdatedChildren.count == 0) { |
|
return; |
|
} |
|
|
|
NSHashTable<RCTShadowView *> *shadowViews = _shadowViewsWithUpdatedChildren; |
|
_shadowViewsWithUpdatedChildren = [NSHashTable weakObjectsHashTable]; |
|
|
|
NSMutableArray *tags = [NSMutableArray arrayWithCapacity:shadowViews.count]; |
|
|
|
for (RCTShadowView *shadowView in shadowViews) { |
|
[shadowView didUpdateReactSubviews]; |
|
[tags addObject:shadowView.reactTag]; |
|
} |
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
for (NSNumber *tag in tags) { |
|
UIView<RCTComponent> *view = viewRegistry[tag]; |
|
[view didUpdateReactSubviews]; |
|
} |
|
}]; |
|
} |
|
|
|
- (void)_dispatchPropsDidChangeEvents |
|
{ |
|
if (_shadowViewsWithUpdatedProps.count == 0) { |
|
return; |
|
} |
|
|
|
NSMapTable<RCTShadowView *, NSArray<NSString *> *> *shadowViews = _shadowViewsWithUpdatedProps; |
|
_shadowViewsWithUpdatedProps = [NSMapTable weakToStrongObjectsMapTable]; |
|
|
|
NSMapTable<NSNumber *, NSArray<NSString *> *> *tags = [NSMapTable strongToStrongObjectsMapTable]; |
|
|
|
for (RCTShadowView *shadowView in shadowViews) { |
|
NSArray<NSString *> *props = [shadowViews objectForKey:shadowView]; |
|
[shadowView didSetProps:props]; |
|
[tags setObject:props forKey:shadowView.reactTag]; |
|
} |
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
for (NSNumber *tag in tags) { |
|
UIView<RCTComponent> *view = viewRegistry[tag]; |
|
[view didSetProps:[tags objectForKey:tag]]; |
|
} |
|
}]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(measure:(nonnull NSNumber *)reactTag |
|
callback:(RCTResponseSenderBlock)callback) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
UIView *view = viewRegistry[reactTag]; |
|
if (!view) { |
|
// this view was probably collapsed out |
|
RCTLogWarn(@"measure cannot find view with tag #%@", reactTag); |
|
callback(@[]); |
|
return; |
|
} |
|
|
|
// If in a <Modal>, rootView will be the root of the modal container. |
|
UIView *rootView = view; |
|
while (rootView.superview && ![rootView isReactRootView]) { |
|
rootView = rootView.superview; |
|
} |
|
|
|
// By convention, all coordinates, whether they be touch coordinates, or |
|
// measurement coordinates are with respect to the root view. |
|
CGRect frame = view.frame; |
|
CGRect globalBounds = [view convertRect:view.bounds toView:rootView]; |
|
|
|
callback(@[ |
|
@(frame.origin.x), |
|
@(frame.origin.y), |
|
@(globalBounds.size.width), |
|
@(globalBounds.size.height), |
|
@(globalBounds.origin.x), |
|
@(globalBounds.origin.y), |
|
]); |
|
}]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(measureInWindow:(nonnull NSNumber *)reactTag |
|
callback:(RCTResponseSenderBlock)callback) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
UIView *view = viewRegistry[reactTag]; |
|
if (!view) { |
|
// this view was probably collapsed out |
|
RCTLogWarn(@"measure cannot find view with tag #%@", reactTag); |
|
callback(@[]); |
|
return; |
|
} |
|
|
|
// Return frame coordinates in window |
|
CGRect windowFrame = [view.window convertRect:view.frame fromView:view.superview]; |
|
callback(@[ |
|
@(windowFrame.origin.x), |
|
@(windowFrame.origin.y), |
|
@(windowFrame.size.width), |
|
@(windowFrame.size.height), |
|
]); |
|
}]; |
|
} |
|
|
|
/** |
|
* Returs if the shadow view provided has the `ancestor` shadow view as |
|
* an actual ancestor. |
|
*/ |
|
RCT_EXPORT_METHOD(viewIsDescendantOf:(nonnull NSNumber *)reactTag |
|
ancestor:(nonnull NSNumber *)ancestorReactTag |
|
callback:(RCTResponseSenderBlock)callback) |
|
{ |
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; |
|
RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag]; |
|
if (!shadowView) { |
|
return; |
|
} |
|
if (!ancestorShadowView) { |
|
return; |
|
} |
|
BOOL viewIsAncestor = [shadowView viewIsDescendantOf:ancestorShadowView]; |
|
callback(@[@(viewIsAncestor)]); |
|
} |
|
|
|
static void RCTMeasureLayout(RCTShadowView *view, |
|
RCTShadowView *ancestor, |
|
RCTResponseSenderBlock callback) |
|
{ |
|
if (!view) { |
|
return; |
|
} |
|
if (!ancestor) { |
|
return; |
|
} |
|
CGRect result = [view measureLayoutRelativeToAncestor:ancestor]; |
|
if (CGRectIsNull(result)) { |
|
RCTLogError(@"view %@ (tag #%@) is not a descendant of %@ (tag #%@)", |
|
view, view.reactTag, ancestor, ancestor.reactTag); |
|
return; |
|
} |
|
CGFloat leftOffset = result.origin.x; |
|
CGFloat topOffset = result.origin.y; |
|
CGFloat width = result.size.width; |
|
CGFloat height = result.size.height; |
|
if (isnan(leftOffset) || isnan(topOffset) || isnan(width) || isnan(height)) { |
|
RCTLogError(@"Attempted to measure layout but offset or dimensions were NaN"); |
|
return; |
|
} |
|
callback(@[@(leftOffset), @(topOffset), @(width), @(height)]); |
|
} |
|
|
|
/** |
|
* Returns the computed recursive offset layout in a dictionary form. The |
|
* returned values are relative to the `ancestor` shadow view. Returns `nil`, if |
|
* the `ancestor` shadow view is not actually an `ancestor`. Does not touch |
|
* anything on the main UI thread. Invokes supplied callback with (x, y, width, |
|
* height). |
|
*/ |
|
RCT_EXPORT_METHOD(measureLayout:(nonnull NSNumber *)reactTag |
|
relativeTo:(nonnull NSNumber *)ancestorReactTag |
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback |
|
callback:(RCTResponseSenderBlock)callback) |
|
{ |
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; |
|
RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag]; |
|
RCTMeasureLayout(shadowView, ancestorShadowView, callback); |
|
} |
|
|
|
/** |
|
* Returns the computed recursive offset layout in a dictionary form. The |
|
* returned values are relative to the `ancestor` shadow view. Returns `nil`, if |
|
* the `ancestor` shadow view is not actually an `ancestor`. Does not touch |
|
* anything on the main UI thread. Invokes supplied callback with (x, y, width, |
|
* height). |
|
*/ |
|
RCT_EXPORT_METHOD(measureLayoutRelativeToParent:(nonnull NSNumber *)reactTag |
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback |
|
callback:(RCTResponseSenderBlock)callback) |
|
{ |
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; |
|
RCTMeasureLayout(shadowView, shadowView.reactSuperview, callback); |
|
} |
|
|
|
/** |
|
* Returns an array of computed offset layouts in a dictionary form. The layouts are of any React subviews |
|
* that are immediate descendants to the parent view found within a specified rect. The dictionary result |
|
* contains left, top, width, height and an index. The index specifies the position among the other subviews. |
|
* Only layouts for views that are within the rect passed in are returned. Invokes the error callback if the |
|
* passed in parent view does not exist. Invokes the supplied callback with the array of computed layouts. |
|
*/ |
|
RCT_EXPORT_METHOD(measureViewsInRect:(CGRect)rect |
|
parentView:(nonnull NSNumber *)reactTag |
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback |
|
callback:(RCTResponseSenderBlock)callback) |
|
{ |
|
RCTShadowView *shadowView = _shadowViewRegistry[reactTag]; |
|
if (!shadowView) { |
|
RCTLogError(@"Attempting to measure view that does not exist (tag #%@)", reactTag); |
|
return; |
|
} |
|
NSArray<RCTShadowView *> *childShadowViews = [shadowView reactSubviews]; |
|
NSMutableArray<NSDictionary *> *results = |
|
[[NSMutableArray alloc] initWithCapacity:childShadowViews.count]; |
|
|
|
[childShadowViews enumerateObjectsUsingBlock: |
|
^(RCTShadowView *childShadowView, NSUInteger idx, __unused BOOL *stop) { |
|
CGRect childLayout = [childShadowView measureLayoutRelativeToAncestor:shadowView]; |
|
if (CGRectIsNull(childLayout)) { |
|
RCTLogError(@"View %@ (tag #%@) is not a descendant of %@ (tag #%@)", |
|
childShadowView, childShadowView.reactTag, shadowView, shadowView.reactTag); |
|
return; |
|
} |
|
|
|
CGFloat leftOffset = childLayout.origin.x; |
|
CGFloat topOffset = childLayout.origin.y; |
|
CGFloat width = childLayout.size.width; |
|
CGFloat height = childLayout.size.height; |
|
|
|
if (leftOffset <= rect.origin.x + rect.size.width && |
|
leftOffset + width >= rect.origin.x && |
|
topOffset <= rect.origin.y + rect.size.height && |
|
topOffset + height >= rect.origin.y) { |
|
|
|
// This view is within the layout rect |
|
NSDictionary *result = @{@"index": @(idx), |
|
@"left": @(leftOffset), |
|
@"top": @(topOffset), |
|
@"width": @(width), |
|
@"height": @(height)}; |
|
|
|
[results addObject:result]; |
|
} |
|
}]; |
|
callback(@[results]); |
|
} |
|
|
|
RCT_EXPORT_METHOD(takeSnapshot:(id /* NSString or NSNumber */)target |
|
withOptions:(NSDictionary *)options |
|
resolve:(RCTPromiseResolveBlock)resolve |
|
reject:(RCTPromiseRejectBlock)reject) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
|
|
// Get view |
|
UIView *view; |
|
if (target == nil || [target isEqual:@"window"]) { |
|
view = RCTKeyWindow(); |
|
} else if ([target isKindOfClass:[NSNumber class]]) { |
|
view = viewRegistry[target]; |
|
if (!view) { |
|
RCTLogError(@"No view found with reactTag: %@", target); |
|
return; |
|
} |
|
} |
|
|
|
// Get options |
|
CGSize size = [RCTConvert CGSize:options]; |
|
NSString *format = [RCTConvert NSString:options[@"format"] ?: @"png"]; |
|
|
|
// Capture image |
|
if (size.width < 0.1 || size.height < 0.1) { |
|
size = view.bounds.size; |
|
} |
|
UIGraphicsBeginImageContextWithOptions(size, NO, 0); |
|
BOOL success = [view drawViewHierarchyInRect:(CGRect){CGPointZero, size} afterScreenUpdates:YES]; |
|
UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); |
|
UIGraphicsEndImageContext(); |
|
|
|
if (!success || !image) { |
|
reject(RCTErrorUnspecified, @"Failed to capture view snapshot.", nil); |
|
return; |
|
} |
|
|
|
// Convert image to data (on a background thread) |
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ |
|
|
|
NSData *data; |
|
if ([format isEqualToString:@"png"]) { |
|
data = UIImagePNGRepresentation(image); |
|
} else if ([format isEqualToString:@"jpeg"]) { |
|
CGFloat quality = [RCTConvert CGFloat:options[@"quality"] ?: @1]; |
|
data = UIImageJPEGRepresentation(image, quality); |
|
} else { |
|
RCTLogError(@"Unsupported image format: %@", format); |
|
return; |
|
} |
|
|
|
// Save to a temp file |
|
NSError *error = nil; |
|
NSString *tempFilePath = RCTTempFilePath(format, &error); |
|
if (tempFilePath) { |
|
if ([data writeToFile:tempFilePath options:(NSDataWritingOptions)0 error:&error]) { |
|
resolve(tempFilePath); |
|
return; |
|
} |
|
} |
|
|
|
// If we reached here, something went wrong |
|
reject(RCTErrorUnspecified, error.localizedDescription, error); |
|
}); |
|
}]; |
|
} |
|
|
|
/** |
|
* JS sets what *it* considers to be the responder. Later, scroll views can use |
|
* this in order to determine if scrolling is appropriate. |
|
*/ |
|
RCT_EXPORT_METHOD(setJSResponder:(nonnull NSNumber *)reactTag |
|
blockNativeResponder:(__unused BOOL)blockNativeResponder) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
_jsResponder = viewRegistry[reactTag]; |
|
if (!_jsResponder) { |
|
RCTLogError(@"Invalid view set to be the JS responder - tag %@", reactTag); |
|
} |
|
}]; |
|
} |
|
|
|
RCT_EXPORT_METHOD(clearJSResponder) |
|
{ |
|
[self addUIBlock:^(__unused RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
_jsResponder = nil; |
|
}]; |
|
} |
|
|
|
- (NSDictionary<NSString *, id> *)constantsToExport |
|
{ |
|
NSMutableDictionary<NSString *, NSDictionary *> *constants = [NSMutableDictionary new]; |
|
NSMutableDictionary<NSString *, NSDictionary *> *directEvents = [NSMutableDictionary new]; |
|
NSMutableDictionary<NSString *, NSDictionary *> *bubblingEvents = [NSMutableDictionary new]; |
|
|
|
[_componentDataByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, RCTComponentData *componentData, __unused BOOL *stop) { |
|
NSMutableDictionary<NSString *, id> *moduleConstants = [NSMutableDictionary new]; |
|
|
|
// Register which event-types this view dispatches. |
|
// React needs this for the event plugin. |
|
NSMutableDictionary<NSString *, NSDictionary *> *bubblingEventTypes = [NSMutableDictionary new]; |
|
NSMutableDictionary<NSString *, NSDictionary *> *directEventTypes = [NSMutableDictionary new]; |
|
|
|
// Add manager class |
|
moduleConstants[@"Manager"] = RCTBridgeModuleNameForClass(componentData.managerClass); |
|
|
|
// Add native props |
|
NSDictionary<NSString *, id> *viewConfig = [componentData viewConfig]; |
|
moduleConstants[@"NativeProps"] = viewConfig[@"propTypes"]; |
|
moduleConstants[@"baseModuleName"] = viewConfig[@"baseModuleName"]; |
|
moduleConstants[@"bubblingEventTypes"] = bubblingEventTypes; |
|
moduleConstants[@"directEventTypes"] = directEventTypes; |
|
|
|
// Add direct events |
|
for (NSString *eventName in viewConfig[@"directEvents"]) { |
|
if (!directEvents[eventName]) { |
|
directEvents[eventName] = @{ |
|
@"registrationName": [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"], |
|
}; |
|
} |
|
directEventTypes[eventName] = directEvents[eventName]; |
|
if (RCT_DEBUG && bubblingEvents[eventName]) { |
|
RCTLogError(@"Component '%@' re-registered bubbling event '%@' as a " |
|
"direct event", componentData.name, eventName); |
|
} |
|
} |
|
|
|
// Add bubbling events |
|
for (NSString *eventName in viewConfig[@"bubblingEvents"]) { |
|
if (!bubblingEvents[eventName]) { |
|
NSString *bubbleName = [eventName stringByReplacingCharactersInRange:(NSRange){0, 3} withString:@"on"]; |
|
bubblingEvents[eventName] = @{ |
|
@"phasedRegistrationNames": @{ |
|
@"bubbled": bubbleName, |
|
@"captured": [bubbleName stringByAppendingString:@"Capture"], |
|
} |
|
}; |
|
} |
|
bubblingEventTypes[eventName] = bubblingEvents[eventName]; |
|
if (RCT_DEBUG && directEvents[eventName]) { |
|
RCTLogError(@"Component '%@' re-registered direct event '%@' as a " |
|
"bubbling event", componentData.name, eventName); |
|
} |
|
} |
|
|
|
RCTAssert(!constants[name], @"UIManager already has constants for %@", componentData.name); |
|
constants[name] = moduleConstants; |
|
}]; |
|
|
|
return constants; |
|
} |
|
|
|
RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config |
|
withCallback:(RCTResponseSenderBlock)callback |
|
errorCallback:(__unused RCTResponseSenderBlock)errorCallback) |
|
{ |
|
RCTLayoutAnimationGroup *layoutAnimationGroup = |
|
[[RCTLayoutAnimationGroup alloc] initWithConfig:config |
|
callback:callback]; |
|
|
|
[self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) { |
|
[uiManager setNextLayoutAnimationGroup:layoutAnimationGroup]; |
|
}]; |
|
} |
|
|
|
- (void)rootViewForReactTag:(NSNumber *)reactTag withCompletion:(void (^)(UIView *view))completion |
|
{ |
|
RCTAssertMainQueue(); |
|
RCTAssert(completion != nil, @"Attempted to resolve rootView for tag %@ without a completion block", reactTag); |
|
|
|
if (reactTag == nil) { |
|
completion(nil); |
|
return; |
|
} |
|
|
|
RCTExecuteOnUIManagerQueue(^{ |
|
NSNumber *rootTag = [self shadowViewForReactTag:reactTag].rootView.reactTag; |
|
RCTExecuteOnMainQueue(^{ |
|
UIView *rootView = nil; |
|
if (rootTag != nil) { |
|
rootView = [self viewForReactTag:rootTag]; |
|
} |
|
completion(rootView); |
|
}); |
|
}); |
|
} |
|
|
|
static UIView *_jsResponder; |
|
|
|
+ (UIView *)JSResponder |
|
{ |
|
return _jsResponder; |
|
} |
|
|
|
@end |
|
|
|
@implementation RCTBridge (RCTUIManager) |
|
|
|
- (RCTUIManager *)uiManager |
|
{ |
|
return [self moduleForClass:[RCTUIManager class]]; |
|
} |
|
|
|
@end
|
|
|