1#import <RNReanimated/REAModule.h> 2#import <RNReanimated/REANodesManager.h> 3#import <React/RCTConvert.h> 4 5#ifdef RCT_NEW_ARCH_ENABLED 6#import <React/RCTComponentViewRegistry.h> 7#import <React/RCTMountingManager.h> 8#import <React/RCTSurfacePresenter.h> 9#import <react/renderer/core/ShadowNode.h> 10#import <react/renderer/uimanager/UIManager.h> 11#else 12#import <stdatomic.h> 13#endif 14 15#if __has_include(<RNScreens/RNSScreenStackHeaderConfig.h>) 16#import <RNScreens/RNSScreenStackHeaderConfig.h> 17#endif 18 19#ifdef RCT_NEW_ARCH_ENABLED 20using namespace facebook::react; 21#endif 22 23// Interface below has been added in order to use private methods of RCTUIManager, 24// RCTUIManager#UpdateView is a React Method which is exported to JS but in 25// Objective-C it stays private 26// RCTUIManager#setNeedsLayout is a method which updated layout only which 27// in its turn will trigger relayout if no batch has been activated 28 29@interface RCTUIManager () 30 31- (void)updateView:(nonnull NSNumber *)reactTag viewName:(NSString *)viewName props:(NSDictionary *)props; 32 33- (void)setNeedsLayout; 34 35@end 36 37@interface RCTUIManager (SyncUpdates) 38 39- (BOOL)hasEnqueuedUICommands; 40 41- (void)runSyncUIUpdatesWithObserver:(id<RCTUIManagerObserver>)observer; 42 43@end 44 45@interface ComponentUpdate : NSObject 46 47@property (nonnull) NSMutableDictionary *props; 48@property (nonnull) NSNumber *viewTag; 49@property (nonnull) NSString *viewName; 50 51@end 52 53@implementation ComponentUpdate 54@end 55 56@implementation RCTUIManager (SyncUpdates) 57 58- (BOOL)hasEnqueuedUICommands 59{ 60 // Accessing some private bits of RCTUIManager to provide missing functionality 61 return [[self valueForKey:@"_pendingUIBlocks"] count] > 0; 62} 63 64- (void)runSyncUIUpdatesWithObserver:(id<RCTUIManagerObserver>)observer 65{ 66 // before we run uimanager batch complete, we override coordinator observers list 67 // to avoid observers from firing. This is done because we only want the uimanager 68 // related operations to run and not all other operations (including the ones enqueued 69 // by reanimated or native animated modules) from being scheduled. If we were to allow 70 // other modules to execute some logic from this sync uimanager run there is a possibility 71 // that the commands will execute out of order or that we intercept a batch of commands that 72 // those modules may be in a middle of (we verify that batch isn't in progress for uimodule 73 // but can't do the same for all remaining modules) 74 75 // store reference to the observers array 76 id oldObservers = [self.observerCoordinator valueForKey:@"_observers"]; 77 78 // temporarily replace observers with a table conatining just nodesmanager (we need 79 // this to capture mounting block) 80 NSHashTable<id<RCTUIManagerObserver>> *soleObserver = [NSHashTable new]; 81 [soleObserver addObject:observer]; 82 [self.observerCoordinator setValue:soleObserver forKey:@"_observers"]; 83 84 // run batch 85 [self batchDidComplete]; 86 // restore old observers table 87 [self.observerCoordinator setValue:oldObservers forKey:@"_observers"]; 88} 89 90@end 91 92#ifndef RCT_NEW_ARCH_ENABLED 93 94@interface REASyncUpdateObserver : NSObject <RCTUIManagerObserver> 95@end 96 97@implementation REASyncUpdateObserver { 98 volatile void (^_mounting)(void); 99 volatile BOOL _waitTimedOut; 100 dispatch_semaphore_t _semaphore; 101} 102 103- (instancetype)init 104{ 105 self = [super init]; 106 if (self) { 107 _mounting = nil; 108 _waitTimedOut = NO; 109 _semaphore = dispatch_semaphore_create(0); 110 } 111 return self; 112} 113 114- (void)dealloc 115{ 116 RCTAssert(_mounting == nil, @"Mouting block was set but never executed. This may lead to UI inconsistencies"); 117} 118 119- (void)unblockUIThread 120{ 121 RCTAssertUIManagerQueue(); 122 dispatch_semaphore_signal(_semaphore); 123} 124 125- (void)waitAndMountWithTimeout:(NSTimeInterval)timeout 126{ 127 RCTAssertMainQueue(); 128 long result = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, timeout * NSEC_PER_SEC)); 129 if (result != 0) { 130 @synchronized(self) { 131 _waitTimedOut = YES; 132 } 133 } 134 if (_mounting) { 135 _mounting(); 136 _mounting = nil; 137 } 138} 139 140- (BOOL)uiManager:(RCTUIManager *)manager performMountingWithBlock:(RCTUIManagerMountingBlock)block 141{ 142 RCTAssertUIManagerQueue(); 143 @synchronized(self) { 144 if (_waitTimedOut) { 145 return NO; 146 } else { 147 _mounting = block; 148 return YES; 149 } 150 } 151} 152 153@end 154 155#endif 156 157@implementation REANodesManager { 158 CADisplayLink *_displayLink; 159 BOOL _wantRunUpdates; 160 NSMutableArray<REAOnAnimationCallback> *_onAnimationCallbacks; 161 BOOL _tryRunBatchUpdatesSynchronously; 162 REAEventHandler _eventHandler; 163 NSMutableDictionary<NSNumber *, ComponentUpdate *> *_componentUpdateBuffer; 164 NSMutableDictionary<NSNumber *, UIView *> *_viewRegistry; 165#ifdef RCT_NEW_ARCH_ENABLED 166 __weak RCTBridge *_bridge; 167 REAPerformOperations _performOperations; 168 __weak id<RCTSurfacePresenterStub> _surfacePresenter; 169 NSMutableDictionary<NSNumber *, NSMutableDictionary *> *_operationsInBatch; 170#else 171 NSMutableArray<REANativeAnimationOp> *_operationsInBatch; 172 volatile atomic_bool _shouldFlushUpdateBuffer; 173#endif 174} 175 176#ifdef RCT_NEW_ARCH_ENABLED 177- (nonnull instancetype)initWithModule:(REAModule *)reanimatedModule 178 bridge:(RCTBridge *)bridge 179 surfacePresenter:(id<RCTSurfacePresenterStub>)surfacePresenter 180{ 181 if ((self = [super init])) { 182 _bridge = bridge; 183 _surfacePresenter = surfacePresenter; 184 _reanimatedModule = reanimatedModule; 185 _wantRunUpdates = NO; 186 _onAnimationCallbacks = [NSMutableArray new]; 187 _operationsInBatch = [NSMutableDictionary new]; 188 _componentUpdateBuffer = [NSMutableDictionary new]; 189 _viewRegistry = [_uiManager valueForKey:@"_viewRegistry"]; 190 } 191 192 _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onAnimationFrame:)]; 193 _displayLink.preferredFramesPerSecond = 120; // will fallback to 60 fps for devices without Pro Motion display 194 [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; 195 [_displayLink setPaused:true]; 196 return self; 197} 198#else 199- (instancetype)initWithModule:(REAModule *)reanimatedModule uiManager:(RCTUIManager *)uiManager 200{ 201 if ((self = [super init])) { 202 _reanimatedModule = reanimatedModule; 203 _uiManager = uiManager; 204 _wantRunUpdates = NO; 205 _onAnimationCallbacks = [NSMutableArray new]; 206 _operationsInBatch = [NSMutableArray new]; 207 _componentUpdateBuffer = [NSMutableDictionary new]; 208 _viewRegistry = [_uiManager valueForKey:@"_viewRegistry"]; 209 _shouldFlushUpdateBuffer = false; 210 } 211 212 _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onAnimationFrame:)]; 213 _displayLink.preferredFramesPerSecond = 120; // will fallback to 60 fps for devices without Pro Motion display 214 [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; 215 [_displayLink setPaused:true]; 216 return self; 217} 218#endif 219 220- (void)invalidate 221{ 222 _eventHandler = nil; 223 [_displayLink invalidate]; 224} 225 226#ifdef RCT_NEW_ARCH_ENABLED 227- (void)setSurfacePresenter:(id<RCTSurfacePresenterStub>)surfacePresenter 228{ 229 _surfacePresenter = surfacePresenter; 230} 231#endif 232 233- (void)operationsBatchDidComplete 234{ 235 if (![_displayLink isPaused]) { 236 // if display link is set it means some of the operations that have run as a part of the batch 237 // requested updates. We want updates to be run in the same frame as in which operations have 238 // been scheduled as it may mean the new view has just been mounted and expects its initial 239 // props to be calculated. 240 // Unfortunately if the operation has just scheduled animation callback it won't run until the 241 // next frame, so it's being triggered manually. 242 _wantRunUpdates = YES; 243 [self performOperations]; 244 } 245} 246 247- (void)postOnAnimation:(REAOnAnimationCallback)clb 248{ 249 [_onAnimationCallbacks addObject:clb]; 250 [self startUpdatingOnAnimationFrame]; 251} 252 253- (void)registerEventHandler:(REAEventHandler)eventHandler 254{ 255 _eventHandler = eventHandler; 256} 257 258#ifdef RCT_NEW_ARCH_ENABLED 259- (void)registerPerformOperations:(REAPerformOperations)performOperations 260{ 261 _performOperations = performOperations; 262} 263#endif 264 265- (void)startUpdatingOnAnimationFrame 266{ 267 // Setting _currentAnimationTimestamp here is connected with manual triggering of performOperations 268 // in operationsBatchDidComplete. If new node has been created and clock has not been started, 269 // _displayLink won't be initialized soon enough and _displayLink.timestamp will be 0. 270 // However, CADisplayLink is using CACurrentMediaTime so if there's need to perform one more 271 // evaluation, it could be used it here. In usual case, CACurrentMediaTime is not being used in 272 // favor of setting it with _displayLink.timestamp in onAnimationFrame method. 273 _currentAnimationTimestamp = CACurrentMediaTime(); 274 [_displayLink setPaused:false]; 275} 276 277- (void)stopUpdatingOnAnimationFrame 278{ 279 if (_displayLink) { 280 [_displayLink setPaused:true]; 281 } 282} 283 284- (void)onAnimationFrame:(CADisplayLink *)displayLink 285{ 286 _currentAnimationTimestamp = _displayLink.timestamp; 287 288 NSArray<REAOnAnimationCallback> *callbacks = _onAnimationCallbacks; 289 _onAnimationCallbacks = [NSMutableArray new]; 290 291 // When one of the callbacks would postOnAnimation callback we don't want 292 // to process it until the next frame. This is why we cpy the array before 293 // we iterate over it 294 for (REAOnAnimationCallback block in callbacks) { 295 block(displayLink); 296 } 297 298 [self performOperations]; 299 300 if (_onAnimationCallbacks.count == 0) { 301 [self stopUpdatingOnAnimationFrame]; 302 } 303} 304 305- (void)performOperations 306{ 307#ifdef RCT_NEW_ARCH_ENABLED 308 _performOperations(); // calls NativeReanimatedModule::performOperations 309 _wantRunUpdates = NO; 310#else 311 if (_operationsInBatch.count != 0) { 312 NSMutableArray<REANativeAnimationOp> *copiedOperationsQueue = _operationsInBatch; 313 _operationsInBatch = [NSMutableArray new]; 314 315 BOOL trySynchronously = _tryRunBatchUpdatesSynchronously; 316 _tryRunBatchUpdatesSynchronously = NO; 317 318 __weak __typeof__(self) weakSelf = self; 319 REASyncUpdateObserver *syncUpdateObserver = [REASyncUpdateObserver new]; 320 RCTExecuteOnUIManagerQueue(^{ 321 __typeof__(self) strongSelf = weakSelf; 322 if (strongSelf == nil) { 323 return; 324 } 325 BOOL canUpdateSynchronously = trySynchronously && ![strongSelf.uiManager hasEnqueuedUICommands]; 326 327 if (!canUpdateSynchronously) { 328 [syncUpdateObserver unblockUIThread]; 329 } 330 331 for (int i = 0; i < copiedOperationsQueue.count; i++) { 332 copiedOperationsQueue[i](strongSelf.uiManager); 333 } 334 335 if (canUpdateSynchronously) { 336 [strongSelf.uiManager runSyncUIUpdatesWithObserver:syncUpdateObserver]; 337 [syncUpdateObserver unblockUIThread]; 338 } 339 // In case canUpdateSynchronously=true we still have to send uiManagerWillPerformMounting event 340 // to observers because some components (e.g. TextInput) update their UIViews only on that event. 341 [strongSelf.uiManager setNeedsLayout]; 342 }); 343 if (trySynchronously) { 344 // The 16ms timeout here aims to match the frame duration. It may make sense to read that parameter 345 // from CADisplayLink but it is easier to hardcode it for the time being. 346 // The reason why we use frame duration here is that if takes longer than one frame to complete layout tasks 347 // there is no point of synchronizing layout with the UI interaction as we get that one frame delay anyways. 348 [syncUpdateObserver waitAndMountWithTimeout:0.016]; 349 } 350 } 351 _wantRunUpdates = NO; 352#endif 353} 354 355#ifdef RCT_NEW_ARCH_ENABLED 356// nothing 357#else 358- (void)enqueueUpdateViewOnNativeThread:(nonnull NSNumber *)reactTag 359 viewName:(NSString *)viewName 360 nativeProps:(NSMutableDictionary *)nativeProps 361 trySynchronously:(BOOL)trySync 362{ 363 if (trySync) { 364 _tryRunBatchUpdatesSynchronously = YES; 365 } 366 [_operationsInBatch addObject:^(RCTUIManager *uiManager) { 367 [uiManager updateView:reactTag viewName:viewName props:nativeProps]; 368 }]; 369} 370#endif 371 372- (void)dispatchEvent:(id<RCTEvent>)event 373{ 374 NSString *key = [NSString stringWithFormat:@"%@%@", event.viewTag, RCTNormalizeInputEventName(event.eventName)]; 375 376 NSString *eventHash = [NSString stringWithFormat:@"%@%@", event.viewTag, event.eventName]; 377 378 if (_eventHandler != nil) { 379 __weak REAEventHandler eventHandler = _eventHandler; 380 __weak __typeof__(self) weakSelf = self; 381 RCTExecuteOnMainQueue(^void() { 382 __typeof__(self) strongSelf = weakSelf; 383 if (strongSelf == nil) { 384 return; 385 } 386 if (eventHandler == nil) { 387 return; 388 } 389 eventHandler(eventHash, event); 390 [strongSelf performOperations]; 391 }); 392 } 393} 394 395#ifdef RCT_NEW_ARCH_ENABLED 396// nothing 397#else 398- (void)configureUiProps:(nonnull NSSet<NSString *> *)uiPropsSet 399 andNativeProps:(nonnull NSSet<NSString *> *)nativePropsSet 400{ 401 _uiProps = uiPropsSet; 402 _nativeProps = nativePropsSet; 403} 404#endif 405 406- (BOOL)isNativeViewMounted:(NSNumber *)viewTag 407{ 408 UIView *view = _viewRegistry[viewTag]; 409 if (view.superview != nil) { 410 return YES; 411 } 412#if __has_include(<RNScreens/RNSScreenStackHeaderConfig.h>) 413 if ([view isKindOfClass:[RNSScreenStackHeaderConfig class]]) { 414 return ((RNSScreenStackHeaderConfig *)view).screenView != nil; 415 } 416#endif 417 return NO; 418} 419 420#ifdef RCT_NEW_ARCH_ENABLED 421 422- (void)synchronouslyUpdateViewOnUIThread:(nonnull NSNumber *)viewTag props:(nonnull NSDictionary *)uiProps 423{ 424 // adapted from RCTPropsAnimatedNode.m 425 RCTSurfacePresenter *surfacePresenter = _bridge.surfacePresenter ?: _surfacePresenter; 426 [surfacePresenter synchronouslyUpdateViewOnUIThread:viewTag props:uiProps]; 427 428 // `synchronouslyUpdateViewOnUIThread` does not flush props like `backgroundColor` etc. 429 // so that's why we need to call `finalizeUpdates` here. 430 RCTComponentViewRegistry *componentViewRegistry = surfacePresenter.mountingManager.componentViewRegistry; 431 UIView<RCTComponentViewProtocol> *componentView = 432 [componentViewRegistry findComponentViewWithTag:[viewTag integerValue]]; 433 [componentView finalizeUpdates:RNComponentViewUpdateMask{}]; 434} 435 436#else 437 438- (void)updateProps:(nonnull NSDictionary *)props 439 ofViewWithTag:(nonnull NSNumber *)viewTag 440 withName:(nonnull NSString *)viewName 441{ 442 ComponentUpdate *lastSnapshot = _componentUpdateBuffer[viewTag]; 443 BOOL isNativeViewMounted = [self isNativeViewMounted:viewTag]; 444 445 if (lastSnapshot != nil) { 446 NSMutableDictionary *lastProps = lastSnapshot.props; 447 for (NSString *key in props) { 448 [lastProps setValue:props[key] forKey:key]; 449 } 450 } 451 452 // If the component isn't mounted, we will bail early with a scheduled update 453 if (!isNativeViewMounted) { 454 if (lastSnapshot == nil) { 455 ComponentUpdate *propsSnapshot = [ComponentUpdate new]; 456 propsSnapshot.props = [props mutableCopy]; 457 propsSnapshot.viewTag = viewTag; 458 propsSnapshot.viewName = viewName; 459 _componentUpdateBuffer[viewTag] = propsSnapshot; 460 atomic_store(&_shouldFlushUpdateBuffer, true); 461 } 462 463 return; 464 } 465 466 // The component may have been mounted with a pending snapshot (due to a race condition), 467 // so we should attempt run the update. Otherwise, the next call to -maybeFlushUpdateBuffer 468 // will only arrive when a new component is mounted (which might be never!) 469 // 470 // If there are 0 remaining items in the buffer, we can skip the run in -maybeFlushUpdateBuffer. 471 if (lastSnapshot != nil && isNativeViewMounted) { 472 props = lastSnapshot.props; 473 viewTag = lastSnapshot.viewTag; 474 viewName = lastSnapshot.viewName; 475 476 [_componentUpdateBuffer removeObjectForKey:viewTag]; 477 478 if (_componentUpdateBuffer.count == 0) { 479 atomic_store(&_shouldFlushUpdateBuffer, false); 480 } 481 } 482 483 // TODO: refactor PropsNode to also use this function 484 NSMutableDictionary *uiProps = [NSMutableDictionary new]; 485 NSMutableDictionary *nativeProps = [NSMutableDictionary new]; 486 NSMutableDictionary *jsProps = [NSMutableDictionary new]; 487 488 void (^addBlock)(NSString *key, id obj, BOOL *stop) = ^(NSString *key, id obj, BOOL *stop) { 489 if ([self.uiProps containsObject:key]) { 490 uiProps[key] = obj; 491 } else if ([self.nativeProps containsObject:key]) { 492 nativeProps[key] = obj; 493 } else { 494 jsProps[key] = obj; 495 } 496 }; 497 498 [props enumerateKeysAndObjectsUsingBlock:addBlock]; 499 500 if (uiProps.count > 0) { 501 [self.uiManager synchronouslyUpdateViewOnUIThread:viewTag viewName:viewName props:uiProps]; 502 } 503 if (nativeProps.count > 0) { 504 [self enqueueUpdateViewOnNativeThread:viewTag viewName:viewName nativeProps:nativeProps trySynchronously:YES]; 505 } 506 if (jsProps.count > 0) { 507 [self.reanimatedModule sendEventWithName:@"onReanimatedPropsChange" 508 body:@{@"viewTag" : viewTag, @"props" : jsProps}]; 509 } 510} 511 512- (NSString *)obtainProp:(nonnull NSNumber *)viewTag propName:(nonnull NSString *)propName 513{ 514 UIView *view = [self.uiManager viewForReactTag:viewTag]; 515 516 NSString *result = 517 [NSString stringWithFormat:@"error: unknown propName %@, currently supported: opacity, zIndex", propName]; 518 519 if ([propName isEqualToString:@"opacity"]) { 520 CGFloat alpha = view.alpha; 521 result = [@(alpha) stringValue]; 522 } else if ([propName isEqualToString:@"zIndex"]) { 523 NSInteger zIndex = view.reactZIndex; 524 result = [@(zIndex) stringValue]; 525 } 526 527 return result; 528} 529 530- (void)maybeFlushUpdateBuffer 531{ 532 RCTAssertUIManagerQueue(); 533 bool shouldFlushUpdateBuffer = atomic_load(&_shouldFlushUpdateBuffer); 534 if (!shouldFlushUpdateBuffer) { 535 return; 536 } 537 538 __weak __typeof__(self) weakSelf = self; 539 [_uiManager addUIBlock:^(__unused RCTUIManager *manager, __unused NSDictionary<NSNumber *, UIView *> *viewRegistry) { 540 __typeof__(self) strongSelf = weakSelf; 541 if (strongSelf == nil) { 542 return; 543 } 544 atomic_store(&strongSelf->_shouldFlushUpdateBuffer, false); 545 NSMutableDictionary *componentUpdateBuffer = [strongSelf->_componentUpdateBuffer copy]; 546 strongSelf->_componentUpdateBuffer = [NSMutableDictionary new]; 547 for (NSNumber *tag in componentUpdateBuffer) { 548 ComponentUpdate *componentUpdate = componentUpdateBuffer[tag]; 549 if (componentUpdate == Nil) { 550 continue; 551 } 552 [strongSelf updateProps:componentUpdate.props 553 ofViewWithTag:componentUpdate.viewTag 554 withName:componentUpdate.viewName]; 555 } 556 [strongSelf performOperations]; 557 }]; 558} 559 560#endif // RCT_NEW_ARCH_ENABLED 561 562- (void)maybeFlushUIUpdatesQueue 563{ 564 if ([_displayLink isPaused]) { 565 [self performOperations]; 566 } 567} 568 569@end 570