diff --git a/React/Base/RCTBridge.h b/React/Base/RCTBridge.h index 2ff4d9c1e..ab853851c 100644 --- a/React/Base/RCTBridge.h +++ b/React/Base/RCTBridge.h @@ -10,6 +10,7 @@ #import #import "RCTBridgeModule.h" +#import "RCTFrameUpdate.h" #import "RCTInvalidating.h" #import "RCTJavaScriptExecutor.h" @@ -122,4 +123,14 @@ static const char *__rct_import_##module##_##method##__ = #module"."#method; */ - (void)reload; +/** + * Add a new observer that will be called on every screen refresh + */ +- (void)addFrameUpdateObserver:(id)observer; + +/** + * Stop receiving screen refresh updates for the given observer + */ +- (void)removeFrameUpdateObserver:(id)observer; + @end diff --git a/React/Base/RCTBridge.m b/React/Base/RCTBridge.m index a6040bfe5..8aa83723c 100644 --- a/React/Base/RCTBridge.m +++ b/React/Base/RCTBridge.m @@ -677,6 +677,73 @@ static NSDictionary *RCTLocalModulesConfig() return localModules; } +@interface RCTDisplayLink : NSObject + +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; + +@end + +@interface RCTBridge (RCTDisplayLink) + +- (void)_update:(CADisplayLink *)displayLink; + +@end + +@implementation RCTDisplayLink +{ + __weak RCTBridge *_bridge; + CADisplayLink *_displayLink; +} + +- (instancetype)initWithBridge:(RCTBridge *)bridge +{ + if ((self = [super init])) { + _bridge = bridge; + _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; + } + return self; +} + +- (BOOL)isValid +{ + return _displayLink != nil; +} + +- (void)invalidate +{ + if (self.isValid) { + [_displayLink invalidate]; + _displayLink = nil; + } +} + +- (void)_update:(CADisplayLink *)displayLink +{ + [_bridge _update:displayLink]; +} + +@end + +@interface RCTFrameUpdate (Private) + +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink; + +@end + +@implementation RCTFrameUpdate + +- (instancetype)initWithDisplayLink:(CADisplayLink *)displayLink +{ + if ((self = [super init])) { + _timestamp = displayLink.timestamp; + _deltaTime = displayLink.duration; + } + return self; +} + +@end + @implementation RCTBridge { RCTSparseArray *_modulesByID; @@ -685,6 +752,8 @@ static NSDictionary *RCTLocalModulesConfig() Class _executorClass; NSURL *_bundleURL; RCTBridgeModuleProviderBlock _moduleProvider; + RCTDisplayLink *_displayLink; + NSMutableSet *_frameUpdateObservers; BOOL _loading; } @@ -711,6 +780,8 @@ static id _latestJSExecutor; _latestJSExecutor = _javaScriptExecutor; _eventDispatcher = [[RCTEventDispatcher alloc] initWithBridge:self]; _shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL); + _displayLink = [[RCTDisplayLink alloc] initWithBridge:self]; + _frameUpdateObservers = [[NSMutableSet alloc] init]; // Register passed-in module instances NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init]; @@ -891,6 +962,9 @@ static id _latestJSExecutor; [_javaScriptExecutor invalidate]; _javaScriptExecutor = nil; + [_displayLink invalidate]; + _frameUpdateObservers = nil; + // Invalidate modules for (id target in _modulesByID.allObjects) { if ([target respondsToSelector:@selector(invalidate)]) { @@ -1075,6 +1149,26 @@ static id _latestJSExecutor; return YES; } +- (void)_update:(CADisplayLink *)displayLink +{ + RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink]; + for (id observer in _frameUpdateObservers) { + if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) { + [observer didUpdateFrame:frameUpdate]; + } + } +} + +- (void)addFrameUpdateObserver:(id)observer +{ + [_frameUpdateObservers addObject:observer]; +} + +- (void)removeFrameUpdateObserver:(id)observer +{ + [_frameUpdateObservers removeObject:observer]; +} + - (void)reload { if (!_loading) { diff --git a/React/Base/RCTFrameUpdate.h b/React/Base/RCTFrameUpdate.h new file mode 100644 index 000000000..b9a3d993f --- /dev/null +++ b/React/Base/RCTFrameUpdate.h @@ -0,0 +1,44 @@ +/** + * 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. + */ + +/** + * Interface containing the information about the last screen refresh. + */ +@interface RCTFrameUpdate : NSObject + +/** + * Timestamp for the actual screen refresh + */ +@property (nonatomic, readonly) NSTimeInterval timestamp; + +/** + * Time since the last frame update ( >= 16.6ms ) + */ +@property (nonatomic, readonly) NSTimeInterval deltaTime; + +@end + +/** + * Protocol that must be implemented for subscribing to display refreshes (DisplayLink updates) + */ +@protocol RCTFrameUpdateObserver + +/** + * Method called on every screen refresh (if paused != YES) + */ +- (void)didUpdateFrame:(RCTFrameUpdate *)update; + +@optional + +/** + * Synthesize and set to true to pause the calls to -[didUpdateFrame:] + */ +@property (nonatomic, assign, getter=isPaused) BOOL paused; + +@end diff --git a/React/Modules/RCTTiming.h b/React/Modules/RCTTiming.h index 67251613b..c6d63bcfc 100644 --- a/React/Modules/RCTTiming.h +++ b/React/Modules/RCTTiming.h @@ -10,8 +10,9 @@ #import #import "RCTBridgeModule.h" +#import "RCTFrameUpdate.h" #import "RCTInvalidating.h" -@interface RCTTiming : NSObject +@interface RCTTiming : NSObject @end diff --git a/React/Modules/RCTTiming.m b/React/Modules/RCTTiming.m index 8c7ef1f23..ce8688f62 100644 --- a/React/Modules/RCTTiming.m +++ b/React/Modules/RCTTiming.m @@ -58,7 +58,6 @@ @implementation RCTTiming { RCTSparseArray *_timers; - id _updateTimer; } @synthesize bridge = _bridge; @@ -113,32 +112,21 @@ RCT_IMPORT_METHOD(RCTJSTimers, callTimers) - (void)stopTimers { - [_updateTimer invalidate]; - _updateTimer = nil; + [_bridge removeFrameUpdateObserver:self]; } - (void)startTimers { RCTAssertMainThread(); - if (![self isValid] || _updateTimer != nil || _timers.count == 0) { + if (![self isValid] || _timers.count == 0) { return; } - _updateTimer = [CADisplayLink displayLinkWithTarget:self selector:@selector(update)]; - if (_updateTimer) { - [_updateTimer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; - } else { - RCTLogWarn(@"Failed to create a display link (probably on buildbot) - using an NSTimer for AppEngine instead."); - _updateTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60) - target:self - selector:@selector(update) - userInfo:nil - repeats:YES]; - } + [_bridge addFrameUpdateObserver:self]; } -- (void)update +- (void)didUpdateFrame:(RCTFrameUpdate *)update { RCTAssertMainThread(); diff --git a/React/React.xcodeproj/project.pbxproj b/React/React.xcodeproj/project.pbxproj index 4c1cfc241..294bf4145 100644 --- a/React/React.xcodeproj/project.pbxproj +++ b/React/React.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ 13E067541A70F44B002CDEE1 /* UIView+React.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+React.m"; sourceTree = ""; }; 14200DA81AC179B3008EE6BA /* RCTJavaScriptLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTJavaScriptLoader.h; sourceTree = ""; }; 14200DA91AC179B3008EE6BA /* RCTJavaScriptLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTJavaScriptLoader.m; sourceTree = ""; }; + 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTFrameUpdate.h; sourceTree = ""; }; 14435CE11AAC4AE100FC20F4 /* RCTMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMap.h; sourceTree = ""; }; 14435CE21AAC4AE100FC20F4 /* RCTMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTMap.m; sourceTree = ""; }; 14435CE31AAC4AE100FC20F4 /* RCTMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTMapManager.h; sourceTree = ""; }; @@ -391,6 +392,7 @@ 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */, 83CBBA4F1A601E3B00E9B192 /* RCTUtils.h */, 83CBBA501A601E3B00E9B192 /* RCTUtils.m */, + 1436DD071ADE7AA000A5ED7D /* RCTFrameUpdate.h */, ); path = Base; sourceTree = ""; diff --git a/React/Views/RCTNavigator.h b/React/Views/RCTNavigator.h index ad7a2fd32..c59c9a3d3 100644 --- a/React/Views/RCTNavigator.h +++ b/React/Views/RCTNavigator.h @@ -9,16 +9,17 @@ #import +#import "RCTFrameUpdate.h" #import "RCTInvalidating.h" -@class RCTEventDispatcher; +@class RCTBridge; -@interface RCTNavigator : UIView +@interface RCTNavigator : UIView @property (nonatomic, strong) UIView *reactNavSuperviewLink; @property (nonatomic, assign) NSInteger requestedTopOfStack; -- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER; /** * Schedules a JavaScript navigation and prevents `UIKit` from navigating until diff --git a/React/Views/RCTNavigator.m b/React/Views/RCTNavigator.m index 5523e49b7..f3ebb6554 100644 --- a/React/Views/RCTNavigator.m +++ b/React/Views/RCTNavigator.m @@ -10,6 +10,7 @@ #import "RCTNavigator.h" #import "RCTAssert.h" +#import "RCTBridge.h" #import "RCTConvert.h" #import "RCTEventDispatcher.h" #import "RCTLog.h" @@ -190,10 +191,6 @@ NSInteger kNeverProgressed = -10000; @end @interface RCTNavigator() -{ - RCTEventDispatcher *_eventDispatcher; - NSInteger _numberOfViewControllerMovesToIgnore; -} @property (nonatomic, assign) NSInteger previousRequestedTopOfStack; @@ -251,7 +248,6 @@ NSInteger kNeverProgressed = -10000; * */ @property (nonatomic, readonly, assign) CGFloat mostRecentProgress; -@property (nonatomic, readwrite, strong) CADisplayLink *displayLink; @property (nonatomic, readonly, strong) NSTimer *runTimer; @property (nonatomic, readonly, assign) NSInteger currentlyTransitioningFrom; @property (nonatomic, readonly, assign) NSInteger currentlyTransitioningTo; @@ -263,22 +259,17 @@ NSInteger kNeverProgressed = -10000; @end @implementation RCTNavigator +{ + __weak RCTBridge *_bridge; + NSInteger _numberOfViewControllerMovesToIgnore; +} -- (id)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher +- (id)initWithBridge:(RCTBridge *)bridge { if ((self = [super initWithFrame:CGRectZero])) { - _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(reportNavigationProgress:)]; + _bridge = bridge; _mostRecentProgress = kNeverProgressed; _dummyView = [[UIView alloc] initWithFrame:CGRectZero]; - if (_displayLink) { - [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; - _displayLink.paused = YES; - } else { - // It's okay to leak this on a build bot. - RCTLogWarn(@"Failed to create a display link (probably on automated build system) - using an NSTimer for AppEngine instead."); - _runTimer = [NSTimer scheduledTimerWithTimeInterval:(1.0 / 60.0) target:self selector:@selector(reportNavigationProgress:) userInfo:nil repeats:YES]; - } - _eventDispatcher = eventDispatcher; _previousRequestedTopOfStack = kNeverRequested; // So that we initialize with a push. _previousViews = @[]; _currentViews = [[NSMutableArray alloc] initWithCapacity:0]; @@ -295,7 +286,7 @@ NSInteger kNeverProgressed = -10000; return self; } -- (void)reportNavigationProgress:(CADisplayLink *)sender +- (void)didUpdateFrame:(RCTFrameUpdate *)update { if (_currentlyTransitioningFrom != _currentlyTransitioningTo) { UIView *topView = _dummyView; @@ -307,7 +298,7 @@ NSInteger kNeverProgressed = -10000; return; } _mostRecentProgress = nextProgress; - [_eventDispatcher sendInputEventWithName:@"topNavigationProgress" body:@{ + [_bridge.eventDispatcher sendInputEventWithName:@"topNavigationProgress" body:@{ @"fromIndex": @(_currentlyTransitioningFrom), @"toIndex": @(_currentlyTransitioningTo), @"progress": @(nextProgress), @@ -350,16 +341,14 @@ NSInteger kNeverProgressed = -10000; _dummyView.frame = (CGRect){{destination}}; _currentlyTransitioningFrom = indexOfFrom; _currentlyTransitioningTo = indexOfTo; - if (indexOfFrom != indexOfTo) { - _displayLink.paused = NO; - } + [_bridge addFrameUpdateObserver:self]; } completion:^(id context) { [weakSelf freeLock]; _currentlyTransitioningFrom = 0; _currentlyTransitioningTo = 0; _dummyView.frame = CGRectZero; - _displayLink.paused = YES; + [_bridge removeFrameUpdateObserver:self]; // Reset the parallel position tracker }]; } @@ -400,19 +389,6 @@ NSInteger kNeverProgressed = -10000; return _currentViews; } -- (BOOL)isValid -{ - return _displayLink != nil; -} - -- (void)invalidate -{ - // Prevent displayLink from retaining the navigator indefinitely - [_displayLink invalidate]; - _displayLink = nil; - _runTimer = nil; -} - - (void)layoutSubviews { [super layoutSubviews]; @@ -430,7 +406,7 @@ NSInteger kNeverProgressed = -10000; - (void)handleTopOfStackChanged { - [_eventDispatcher sendInputEventWithName:@"topNavigateBack" body:@{ + [_bridge.eventDispatcher sendInputEventWithName:@"topNavigateBack" body:@{ @"target":self.reactTag, @"stackLength":@(_navigationController.viewControllers.count) }]; @@ -438,7 +414,7 @@ NSInteger kNeverProgressed = -10000; - (void)dispatchFakeScrollEvent { - [_eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove + [_bridge.eventDispatcher sendScrollEventWithType:RCTScrollEventTypeMove reactTag:self.reactTag scrollView:nil userData:nil]; @@ -511,7 +487,7 @@ NSInteger kNeverProgressed = -10000; if (jsGettingAhead) { if (reactPushOne) { UIView *lastView = [_currentViews lastObject]; - RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_eventDispatcher]; + RCTWrapperViewController *vc = [[RCTWrapperViewController alloc] initWithNavItem:(RCTNavItem *)lastView eventDispatcher:_bridge.eventDispatcher]; vc.navigationListener = self; _numberOfViewControllerMovesToIgnore = 1; [_navigationController pushViewController:vc animated:(currentReactCount > 1)]; diff --git a/React/Views/RCTNavigatorManager.m b/React/Views/RCTNavigatorManager.m index 730380bf9..1158f7dcf 100644 --- a/React/Views/RCTNavigatorManager.m +++ b/React/Views/RCTNavigatorManager.m @@ -21,7 +21,7 @@ RCT_EXPORT_MODULE() - (UIView *)view { - return [[RCTNavigator alloc] initWithEventDispatcher:self.bridge.eventDispatcher]; + return [[RCTNavigator alloc] initWithBridge:self.bridge]; } RCT_EXPORT_VIEW_PROPERTY(requestedTopOfStack, NSInteger)