react-native/React/Modules/RCTUIManager.m

1574 lines
55 KiB
Mathematica
Raw Normal View History

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#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"
2015-04-21 12:26:51 +00:00
#import "RCTDefines.h"
[ReactNative] Introduce onLayout events Summary: Simply add an `onLayout` callback to a native view component, and the callback will be invoked with the current layout information when the view is mounted and whenever the layout changes. The only limitation is that scroll position and other stuff the layout system isn't aware of is not taken into account. This is because onLayout events wouldn't be triggered for these changes and if they are desired they should be tracked separately (e.g. with `onScroll`) and combined. Also fixes some bugs with LayoutAnimation callbacks. @public Test Plan: - Run new LayoutEventsExample in UIExplorer and see it work correctly. - New integration test passes internally (IntegrationTest project seems busted). - New jest test case passes. {F22318433} ``` 2015-05-06 15:45:05.848 [info][tid:com.facebook.React.JavaScript] "Running application "UIExplorerApp" with appParams: {"rootTag":1,"initialProps":{}}. __DEV__ === true, development-level warning are ON, performance optimizations are OFF" 2015-05-06 15:45:05.881 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":123,"x":12.5,"width":140.5,"height":18}} 2015-05-06 15:45:05.882 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":122,"width":50,"height":50}} 2015-05-06 15:45:05.883 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":70.5,"x":20,"width":294,"height":204}} 2015-05-06 15:45:05.897 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":206.5,"x":12.5,"width":140.5,"height":18}} 2015-05-06 15:45:05.897 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":70.5,"x":20,"width":294,"height":287.5}} 2015-05-06 15:45:09.847 [info][tid:com.facebook.React.JavaScript] "layout animation done." 2015-05-06 15:45:09.847 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":82,"width":50,"height":50}} 2015-05-06 15:45:09.848 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":110.5,"x":60,"width":214,"height":287.5}} 2015-05-06 15:45:09.862 [info][tid:com.facebook.React.JavaScript] "received text layout event ", {"target":27,"layout":{"y":206.5,"x":12.5,"width":120,"height":68}} 2015-05-06 15:45:09.863 [info][tid:com.facebook.React.JavaScript] "received image layout event ", {"target":23,"layout":{"y":12.5,"x":55,"width":50,"height":50}} 2015-05-06 15:45:09.863 [info][tid:com.facebook.React.JavaScript] "received view layout event ", {"target":22,"layout":{"y":128,"x":60,"width":160,"height":337.5}} ```
2015-05-07 19:11:02 +00:00
#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"
2015-03-26 09:58:06 +00:00
#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
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
NSMutableDictionary<NSNumber *, RCTShadowView *> *_shadowViewRegistry; // RCT thread only
NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; // Main thread only
// Keyed by viewName
NSDictionary *_componentDataByName;
NSMutableSet<id<RCTComponent>> *_bridgeTransactionListeners;
}
2015-04-02 14:33:21 +00:00
@synthesize bridge = _bridge;
2015-04-08 15:52:48 +00:00
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];
}
}
2015-04-11 22:08:00 +00:00
self->_rootViewTags = nil;
self->_shadowViewRegistry = nil;
self->_viewRegistry = nil;
self->_bridgeTransactionListeners = nil;
self->_bridge = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self];
RCT_PROFILE_END_EVENT(RCTProfileTagAlways, @"");
});
}
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
- (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;
}
2015-04-11 22:08:00 +00:00
- (void)setBridge:(RCTBridge *)bridge
{
RCTAssert(_bridge == nil, @"Should not re-use same UIIManager instance");
_bridge = bridge;
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
_shadowViewRegistry = [NSMutableDictionary new];
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
_viewRegistry = [NSMutableDictionary new];
// Internal resources
_pendingUIBlocks = [NSMutableArray new];
_rootViewTags = [NSMutableSet new];
_bridgeTransactionListeners = [NSMutableSet new];
_observerCoordinator = [RCTUIManagerObserverCoordinator new];
2015-04-11 22:08:00 +00:00
// Get view managers from bridge
NSMutableDictionary *componentDataByName = [NSMutableDictionary new];
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
for (Class moduleClass in _bridge.moduleClasses) {
if ([moduleClass isSubclassOfClass:[RCTViewManager class]]) {
RCTComponentData *componentData = [[RCTComponentData alloc] initWithManagerClass:moduleClass
bridge:_bridge];
componentDataByName[componentData.name] = componentData;
2015-04-11 22:08:00 +00:00
}
}
2015-04-11 22:08:00 +00:00
_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];
2015-04-11 22:08:00 +00:00
}
#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;
2015-03-25 00:37:03 +00:00
RCTAssert(RCTIsReactRootView(reactTag),
@"View %@ with tag #%@ is not a root view", rootView, reactTag);
UIView *existingView = _viewRegistry[reactTag];
2015-04-11 22:08:00 +00:00
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.backgroundColor = rootView.backgroundColor;
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
2015-03-25 00:37:03 +00:00
{
RCTAssertMainQueue();
RCTExecuteOnUIManagerQueue(^{
RCTShadowView *shadowView = self->_shadowViewRegistry[tag];
2015-03-25 00:37:03 +00:00
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];
2015-03-25 00:37:03 +00:00
}
- (void)setIntrinsicContentSize:(CGSize)intrinsicContentSize forView:(UIView *)view
{
RCTAssertMainQueue();
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) {
if (CGSizeEqualToSize(shadowView.intrinsicContentSize, intrinsicContentSize)) {
return;
}
shadowView.intrinsicContentSize = intrinsicContentSize;
} forTag:view.reactTag];
}
- (void)setBackgroundColor:(UIColor *)color forView:(UIView *)view
{
RCTAssertMainQueue();
[self _executeBlockWithShadowView:^(RCTShadowView *shadowView) {
if (!self->_viewRegistry) {
return;
}
shadowView.backgroundColor = color;
[self _amendPendingUIBlocksWithStylePropagationUpdateForShadowView:shadowView];
[self flushUIBlocksWithCompletion:^{}];
} 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];
if (registry == (NSMutableDictionary<NSNumber *, id<RCTComponent>> *)self->_viewRegistry) {
[self->_bridgeTransactionListeners removeObject:subview];
}
});
}
}
- (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();
// This is nuanced. In the JS thread, we create a new update buffer
// `frameTags`/`frames` that is created/mutated in the JS thread. We access
// these structures in the UI-thread block. `NSMutableArray` is not thread
// safe so we rely on the fact that we never mutate it after it's passed to
// the main thread.
NSSet<RCTShadowView *> *viewsWithNewFrames = [rootShadowView collectViewsWithUpdatedFrames];
if (!viewsWithNewFrames.count) {
// no frame change results in no UI update block
return nil;
}
typedef struct {
CGRect frame;
UIUserInterfaceLayoutDirection layoutDirection;
BOOL isNew;
BOOL parentIsNew;
BOOL isHidden;
} RCTFrameData;
// Construct arrays then hand off to main thread
NSUInteger count = viewsWithNewFrames.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 viewsWithNewFrames) {
reactTags[index] = shadowView.reactTag;
frameDataArray[index++] = (RCTFrameData){
shadowView.frame,
shadowView.layoutDirection,
shadowView.isNewView,
shadowView.superview.isNewView,
shadowView.isHidden,
};
}
}
2015-06-01 15:34:09 +00:00
// These are blocks to be executed on each view, immediately after
// reactSetFrame: has been called. Note that if reactSetFrame: is not called,
// these won't be called either, so this is not a suitable place to update
// properties that aren't related to layout.
NSMutableDictionary<NSNumber *, RCTViewManagerUIBlock> *updateBlocks =
[NSMutableDictionary new];
for (RCTShadowView *shadowView in viewsWithNewFrames) {
// We have to do this after we build the parentsAreNew array.
shadowView.newView = NO;
NSNumber *reactTag = shadowView.reactTag;
RCTViewManager *manager = [_componentDataByName[shadowView.viewName] manager];
RCTViewManagerUIBlock block = [manager uiBlockToAmendWithShadowView:shadowView];
if (block) {
updateBlocks[reactTag] = block;
}
if (shadowView.onLayout) {
CGRect frame = shadowView.frame;
shadowView.onLayout(@{
@"layout": @{
@"x": @(frame.origin.x),
@"y": @(frame.origin.y),
@"width": @(frame.size.width),
@"height": @(frame.size.height),
},
});
}
if (RCTIsReactRootView(reactTag)) {
CGSize contentSize = shadowView.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;
BOOL isHidden = frameData.isHidden;
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.isHidden != isHidden) {
view.hidden = isHidden;
}
if (view.reactLayoutDirection != layoutDirection) {
view.reactLayoutDirection = layoutDirection;
}
RCTViewManagerUIBlock updateBlock = updateBlocks[reactTag];
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;
}
if (updateBlock) {
updateBlock(self, viewRegistry);
}
} withCompletionBlock:completion];
} else if (updatingLayoutAnimation) {
// Animate view update
[updatingLayoutAnimation performAnimations:^{
[view reactSetFrame:frame];
if (updateBlock) {
updateBlock(self, viewRegistry);
}
} withCompletionBlock:completion];
} else {
// Update without animation
[view reactSetFrame:frame];
if (updateBlock) {
updateBlock(self, viewRegistry);
}
completion(YES);
}
}
// Clean up
uiManager->_layoutAnimationGroup = nil;
};
}
- (void)_amendPendingUIBlocksWithStylePropagationUpdateForShadowView:(RCTShadowView *)topView
{
NSMutableSet<RCTApplierBlock> *applierBlocks = [NSMutableSet setWithCapacity:1];
[topView collectUpdatedProperties:applierBlocks parentProperties:@{}];
if (applierBlocks.count) {
[self addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
for (RCTApplierBlock block in applierBlocks) {
block(viewRegistry);
}
}];
}
}
/**
* 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];
2015-04-11 22:08:00 +00:00
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 (conseptually) 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);
}];
}
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];
}];
}
- (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
2015-04-08 15:52:48 +00:00
viewName:(NSString *)viewName
rootTag:(nonnull NSNumber *)rootTag
2015-04-08 15:52:48 +00:00
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;
}
// Shadow view is the source of truth for background color this is a little
// bit counter-intuitive if people try to set background color when setting up
// the view, but it's the only way that makes sense given our threading model
UIColor *backgroundColor = shadowView.backgroundColor;
// Dispatch view creation directly to the main thread instead of adding to
// UIBlocks array. This way, it doesn't get deferred until after layout.
__weak RCTUIManager *weakManager = self;
RCTExecuteOnMainQueue(^{
RCTUIManager *uiManager = weakManager;
if (!uiManager) {
return;
}
UIView *view = [componentData createViewWithTag:reactTag];
if (view) {
[componentData setProps:props forView:view]; // Must be done before bgColor to prevent wrong default
if ([view respondsToSelector:@selector(setBackgroundColor:)]) {
((UIView *)view).backgroundColor = backgroundColor;
}
if ([view respondsToSelector:@selector(reactBridgeDidFinishTransaction)]) {
[uiManager->_bridgeTransactionListeners addObject:view];
}
uiManager->_viewRegistry[reactTag] = view;
#if RCT_DEV
[view _DEBUG_setReactShadowView:shadowView];
#endif
}
});
}
2015-04-11 22:08:00 +00:00
RCT_EXPORT_METHOD(updateView:(nonnull NSNumber *)reactTag
2015-08-11 22:12:55 +00:00
viewName:(NSString *)viewName // not always reliable, use shadowView.viewName if available
2015-04-08 15:52:48 +00:00
props:(NSDictionary *)props)
{
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
2015-08-11 22:12:55 +00:00
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];
}];
}
- (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
{
2015-05-29 17:27:14 +00:00
// Gather blocks to be executed now that all view hierarchy manipulations have
// been completed (note that these may still take place before layout has finished)
for (RCTComponentData *componentData in _componentDataByName.allValues) {
RCTViewManagerUIBlock uiBlock = [componentData uiBlockToAmendWithShadowViewRegistry:_shadowViewRegistry];
2015-05-29 17:27:14 +00:00
[self addUIBlock:uiBlock];
}
[_observerCoordinator uiManagerWillPerformLayout:self];
// Perform layout
for (NSNumber *reactTag in _rootViewTags) {
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
[self addUIBlock:[self uiBlockWithLayoutUpdateForRootView:rootView]];
}
[_observerCoordinator uiManagerDidPerformLayout:self];
// Properies propagation
for (NSNumber *reactTag in _rootViewTags) {
RCTRootShadowView *rootView = (RCTRootShadowView *)_shadowViewRegistry[reactTag];
[self _amendPendingUIBlocksWithStylePropagationUpdateForShadowView:rootView];
}
[self addUIBlock:^(RCTUIManager *uiManager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) {
/**
* TODO(tadeu): Remove it once and for all
*/
for (id<RCTComponent> node in uiManager->_bridgeTransactionListeners) {
[node reactBridgeDidFinishTransaction];
}
}];
[_observerCoordinator uiManagerWillPerformMounting:self];
[self flushUIBlocksWithCompletion:^{
[self->_observerCoordinator uiManagerDidPerformMounting:self];
}];
2015-03-25 00:37:03 +00:00
}
- (void)flushUIBlocksWithCompletion:(void (^)(void))completion;
2015-03-25 00:37:03 +00:00
{
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;
}
// Execute the previously queued UI blocks
RCTProfileBeginFlowEvent();
RCTExecuteOnMainQueue(^{
RCTProfileEndFlowEvent();
RCT_PROFILE_BEGIN_EVENT(RCTProfileTagAlways, @"-[UIManager flushUIBlocks]", (@{
@"count": [@(previousPendingUIBlocks.count) stringValue],
}));
@try {
for (RCTViewManagerUIBlock block in previousPendingUIBlocks) {
block(self, self->_viewRegistry);
}
}
@catch (NSException *exception) {
RCTLogError(@"Exception thrown while executing UI block: %@", exception);
}
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 immidiately trigger layout.
if (![_bridge isBatchActive] && ![_bridge isLoading]) {
[self _layoutAndMount];
}
}
RCT_EXPORT_METHOD(measure:(nonnull NSNumber *)reactTag
2015-04-08 15:52:48 +00:00
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)]);
}
2015-03-25 00:37:03 +00:00
static void RCTMeasureLayout(RCTShadowView *view,
RCTShadowView *ancestor,
RCTResponseSenderBlock callback)
{
if (!view) {
return;
}
if (!ancestor) {
return;
}
2015-03-25 00:37:03 +00:00
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;
}
2015-03-25 00:37:03 +00:00
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
2015-04-08 15:52:48 +00:00
callback:(RCTResponseSenderBlock)callback)
{
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
RCTShadowView *ancestorShadowView = _shadowViewRegistry[ancestorReactTag];
2015-03-25 00:37:03 +00:00
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
2015-04-08 15:52:48 +00:00
callback:(RCTResponseSenderBlock)callback)
{
RCTShadowView *shadowView = _shadowViewRegistry[reactTag];
2015-03-25 00:37:03 +00:00
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
2015-04-08 15:52:48 +00:00
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];
2015-04-19 19:55:46 +00:00
[childShadowViews enumerateObjectsUsingBlock:
^(RCTShadowView *childShadowView, NSUInteger idx, __unused BOOL *stop) {
2015-03-25 00:37:03 +00:00
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;
2015-04-19 19:55:46 +00:00
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);
}
}];
}
2015-04-08 15:52:48 +00:00
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"];
Support native ViewManager inheritance on iOS Summary: **Motivation** This is a re-worked version of #14260, by shergin's suggestion. For iOS, if you want to inherit from a native ViewManagers, your custom ViewManager will not automatically export the parents' props. So the only way to do this today, is to basically copy/paste the parent ViewManager-file, and add your own custom logic. With this PR, this is made more extensible by exporting the `baseModuleName` (i.e. the iOS `superclass` of the ViewManager), and then using that value to re-establish the inheritance relationship in `requireNativeComponent`. **Test plan** I've run this with a test project, and it works fine there. But needs more testing. Opened this PR as [per shergin's suggestion](https://github.com/facebook/react-native/pull/10946#issuecomment-311860545) though, so we can discuss approach. **Discussion** * Android already supports inheritance, so this change should be compatible with that. But, not every prop available on `UIManager.RCTView.NativeProps` is actually exported by every ViewManager. So should `UIManager.RCTView.NativeProps` still be merged with `viewConfig.NativeProps`, even if the individual ViewManager does not export/use them to begin with? * Does this break other platforms? [UWP](https://github.com/Microsoft/react-native-windows)? Closes https://github.com/facebook/react-native/pull/14775 Differential Revision: D5392953 Pulled By: shergin fbshipit-source-id: 5212da616acfba50cc285e2997d183cf8b2cd09f
2017-07-10 22:48:34 +00:00
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;
}
2015-04-08 15:52:48 +00:00
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
{
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
return [self moduleForClass:[RCTUIManager class]];
}
@end