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