diff --git a/docs/screen-api.md b/docs/screen-api.md index c015df0e746..52871761e7e 100644 --- a/docs/screen-api.md +++ b/docs/screen-api.md @@ -17,7 +17,17 @@ this.props.navigator.push({ backButtonTitle: undefined, // override the back button title (optional) backButtonHidden: false, // hide the back button altogether (optional) navigatorStyle: {}, // override the navigator style for the pushed screen (optional) - navigatorButtons: {} // override the nav buttons for the pushed screen (optional) + navigatorButtons: {}, // override the nav buttons for the pushed screen (optional) + // enable peek and pop - commited screen will have `isPreview` prop set as true. + previewView: undefined, // react ref or node id (optional) + previewHeight: undefined, // set preview height, defaults to full height (optional) + previewCommit: true, // commit to push preview controller to the navigation stack (optional) + previewActions: [{ // action presses can be detected with the `PreviewActionPress` event on the commited screen. + id: '', // action id (required) + title: '', // action title (required) + style: undefined, // 'selected' or 'destructive' (optional) + actions: [], // list of sub-actions + }], }); ``` @@ -295,6 +305,8 @@ export default class ExampleScreen extends Component { break; case 'didDisappear': break; + case 'willCommitPreview': + break; } } } @@ -348,3 +360,12 @@ export default class ExampleScreen extends Component { } } ``` + +# Peek and pop (3D touch) + +react-native-navigation supports the [Peek and pop]( +https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/Adopting3DTouchOniPhone/#//apple_ref/doc/uid/TP40016543-CH1-SW3) feature by setting a react view reference as a `previewView` parameter when doing a push, more options are available in the `push` section. + +You can define actions and listen for interactions on the pushed screen with the `PreviewActionPress` event. + +Previewed screens will have the prop `isPreview` that can be used to render different things when the screen is in the "Peek" state and will then recieve a navigator event of `willCommitPreview` when in the "Pop" state. \ No newline at end of file diff --git a/example/src/components/Row.js b/example/src/components/Row.js index 4857870eff2..97e484b9da9 100644 --- a/example/src/components/Row.js +++ b/example/src/components/Row.js @@ -2,27 +2,32 @@ import React from 'react'; import PropTypes from 'prop-types'; import {StyleSheet, View, Text, TouchableHighlight, Platform} from 'react-native'; -function Row({title, onPress, platform, testID}) { - if (platform && platform !== Platform.OS) { - return ; - } +class Row extends React.PureComponent { + render() { + const {title, onPress, onPressIn, platform, testID} = this.props; + if (platform && platform !== Platform.OS) { + return ; + } - return ( - - - {title} - - - ); + return ( + + + {title} + + + ); + } } Row.propTypes = { title: PropTypes.string.isRequired, onPress: PropTypes.func.isRequired, + onPressIn: PropTypes.func }; const styles = StyleSheet.create({ diff --git a/example/src/screens/NavigationTypes.js b/example/src/screens/NavigationTypes.js index b9214f99459..4973829f74f 100644 --- a/example/src/screens/NavigationTypes.js +++ b/example/src/screens/NavigationTypes.js @@ -34,6 +34,28 @@ class NavigationTypes extends React.Component { }); }; + previewScreen = () => { + this.props.navigator.push({ + screen: 'example.Types.Push', + title: 'New Screen', + previewCommit: true, + previewHeight: 250, + previewView: this.previewRef, + previewActions: [{ + id: 'action-cancel', + title: 'Cancel' + }, { + id: 'action-delete', + title: 'Delete', + actions: [{ + id: 'action-delete-sure', + title: 'Are you sure?', + style: 'destructive' + }] + }] + }); + }; + pushListScreen = () => { console.log('RANG', 'pushListScreen'); this.props.navigator.push({ @@ -107,6 +129,13 @@ class NavigationTypes extends React.Component { + (this.previewRef = ref)} + title={'Preview Screen'} + testID={'previewScreen'} + onPress={this.pushScreen} + onPressIn={this.previewScreen} + /> {/**/} diff --git a/example/src/screens/types/Push.js b/example/src/screens/types/Push.js index e9c2c4cefc4..8a5bde6d2a8 100644 --- a/example/src/screens/types/Push.js +++ b/example/src/screens/types/Push.js @@ -1,8 +1,24 @@ import React, {Component} from 'react'; -import {StyleSheet, View, Text, Button} from 'react-native'; +import {StyleSheet, View, Text, Button, Alert} from 'react-native'; class Push extends Component { + constructor(props) { + super(props); + this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this)); + } + + onNavigatorEvent(event) { + if (event.type === 'PreviewActionPress') { + if (event.id === 'action-cancel') { + Alert.alert('Cancelled'); + } + if (event.id === 'action-delete-sure') { + Alert.alert('Deleted'); + } + } + } + onPushAnother = () => { this.props.navigator.push({ screen: 'example.Types.Push', diff --git a/ios/RCCNavigationController.m b/ios/RCCNavigationController.m index dda66881a93..4bdbffa0664 100755 --- a/ios/RCCNavigationController.m +++ b/ios/RCCNavigationController.m @@ -2,6 +2,10 @@ #import "RCCViewController.h" #import "RCCManager.h" #import +#import +#if __has_include() +#import +#endif #import #import #import @@ -160,6 +164,33 @@ - (void)performAction:(NSString*)performAction actionParams:(NSDictionary*)actio { [self setButtons:rightButtons viewController:viewController side:@"right" animated:NO]; } + + NSArray *previewActions = actionParams[@"previewActions"]; + NSString *previewViewID = actionParams[@"previewViewID"]; + if (previewViewID) { + if ([self.topViewController isKindOfClass:[RCCViewController class]]) + { + RCCViewController *topViewController = ((RCCViewController*)self.topViewController); + viewController.previewActions = previewActions; + viewController.previewCommit = actionParams[@"previewCommit"] ? [actionParams[@"previewCommit"] boolValue] : YES; + NSNumber *previewHeight = actionParams[@"previewHeight"]; + if (previewHeight) { + viewController.preferredContentSize = CGSizeMake(viewController.view.frame.size.width, [previewHeight floatValue]); + } + if (topViewController.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) + { + dispatch_async(RCTGetUIManagerQueue(), ^{ + [bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + UIView *view = viewRegistry[previewViewID]; + topViewController.previewView = view; + [topViewController registerForPreviewingWithDelegate:(id)topViewController sourceView:view]; + }]; + }); + topViewController.previewController = viewController; + } + return; + } + } NSString *animationType = actionParams[@"animationType"]; if ([animationType isEqualToString:@"fade"]) diff --git a/ios/RCCViewController.h b/ios/RCCViewController.h index 1978783725d..27ca96d415a 100755 --- a/ios/RCCViewController.h +++ b/ios/RCCViewController.h @@ -18,6 +18,10 @@ extern NSString* const RCCViewControllerCancelReactTouchesNotification; @property (nonatomic, strong) NSString *controllerId; @property (nonatomic, strong) NSString *commandType; @property (nonatomic, strong) NSString *timestamp; +@property (nonatomic) RCCViewController *previewController; +@property (nonatomic) UIView *previewView; +@property (nonatomic) NSArray *previewActions; +@property (nonatomic) BOOL previewCommit; + (UIViewController*)controllerWithLayout:(NSDictionary *)layout globalProps:(NSDictionary *)globalProps bridge:(RCTBridge *)bridge; diff --git a/ios/RCCViewController.m b/ios/RCCViewController.m index b0222f1def4..964e8a9d0f6 100755 --- a/ios/RCCViewController.m +++ b/ios/RCCViewController.m @@ -19,7 +19,7 @@ const NSInteger BLUR_NAVBAR_TAG = 78264802; const NSInteger TRANSPARENT_NAVBAR_TAG = 78264803; -@interface RCCViewController() +@interface RCCViewController() @property (nonatomic) BOOL _hidesBottomBarWhenPushed; @property (nonatomic) BOOL _statusBarHideWithNavBar; @property (nonatomic) BOOL _statusBarHidden; @@ -794,6 +794,37 @@ -(void)addExternalVCIfNecessary:(NSDictionary*)props } } +#pragma mark - Preview Actions + +- (void)onActionPress:(NSString *)id { + if ([self.view isKindOfClass:[RCTRootView class]]) { + RCTRootView *rootView = (RCTRootView *)self.view; + if (rootView.appProperties && rootView.appProperties[@"navigatorEventID"]) { + [[[RCCManager sharedInstance] getBridge].eventDispatcher + sendAppEventWithName:rootView.appProperties[@"navigatorEventID"] + body:@{ + @"type": @"PreviewActionPress", + @"id": id + }]; + } + } +} + +- (UIPreviewAction *) convertAction:(NSDictionary *)action { + NSString *actionId = action[@"id"]; + NSString *actionTitle = action[@"title"]; + UIPreviewActionStyle actionStyle = UIPreviewActionStyleDefault; + if ([action[@"style"] isEqualToString:@"selected"]) { + actionStyle = UIPreviewActionStyleSelected; + } + if ([action[@"style"] isEqualToString:@"destructive"]) { + actionStyle = UIPreviewActionStyleDestructive; + } + return [UIPreviewAction actionWithTitle:actionTitle style:actionStyle handler:^(UIPreviewAction * _Nonnull action, UIViewController * _Nonnull previewViewController) { + [self onActionPress:actionId]; + }]; +} + #pragma mark - NewRelic - (NSString*) customNewRelicInteractionName @@ -830,4 +861,36 @@ -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecogniz return !disabledSimultaneousGestureBool; } +#pragma mark - UIViewControllerPreviewingDelegate +- (UIViewController *)previewingContext:(id)previewingContext viewControllerForLocation:(CGPoint)location { + return self.previewController; +} + +- (void)previewingContext:(id)previewingContext commitViewController:(UIViewController *)viewControllerToCommit { + if (self.previewController.previewCommit == YES) { + [self.previewController sendGlobalScreenEvent:@"willCommitPreview" endTimestampString:[self.previewController getTimestampString] shouldReset:YES]; + [self.previewController sendScreenChangedEvent:@"willCommitPreview"]; + [self.navigationController pushViewController:self.previewController animated:false]; + } +} + +- (NSArray> *)previewActionItems { + NSMutableArray *actions = [[NSMutableArray alloc] init]; + for (NSDictionary *previewAction in self.previewActions) { + UIPreviewAction *action = [self convertAction:previewAction]; + NSDictionary *actionActions = previewAction[@"actions"]; + if (actionActions.count > 0) { + NSMutableArray *group = [[NSMutableArray alloc] init]; + for (NSDictionary *previewGroupAction in actionActions) { + [group addObject:[self convertAction:previewGroupAction]]; + } + UIPreviewActionGroup *actionGroup = [UIPreviewActionGroup actionGroupWithTitle:action.title style:UIPreviewActionStyleDefault actions:group]; + [actions addObject:actionGroup]; + } else { + [actions addObject:action]; + } + } + return actions; +} + @end diff --git a/src/deprecated/platformSpecificDeprecated.ios.js b/src/deprecated/platformSpecificDeprecated.ios.js index 867c2b54397..6531ae52518 100644 --- a/src/deprecated/platformSpecificDeprecated.ios.js +++ b/src/deprecated/platformSpecificDeprecated.ios.js @@ -1,4 +1,6 @@ /*eslint-disable*/ +import { Component } from 'react'; +import { findNodeHandle } from 'react-native'; import Navigation from './../Navigation'; import Controllers, {Modal, Notification, ScreenUtils} from './controllers'; const React = Controllers.hijackReact(); @@ -234,7 +236,15 @@ function navigatorPush(navigator, params) { console.error('Navigator.push(params): params.screen is required'); return; } + let previewViewID; const screenInstanceID = _.uniqueId('screenInstanceID'); + if (params.previewView instanceof Component) { + previewViewID = findNodeHandle(params.previewView) + } else if (typeof params.previewView === 'number') { + previewViewID = params.previewView; + } else if (params.previewView) { + console.error('Navigator.push(params): params.previewView is not a valid react view'); + } const { navigatorStyle, navigatorButtons, @@ -246,6 +256,8 @@ function navigatorPush(navigator, params) { passProps.navigatorID = navigator.navigatorID; passProps.screenInstanceID = screenInstanceID; passProps.navigatorEventID = navigatorEventID; + passProps.previewViewID = previewViewID; + passProps.isPreview = !!previewViewID; params.navigationParams = { screenInstanceID, @@ -270,6 +282,10 @@ function navigatorPush(navigator, params) { backButtonHidden: params.backButtonHidden, leftButtons: navigatorButtons.leftButtons, rightButtons: navigatorButtons.rightButtons, + previewViewID: previewViewID, + previewActions: params.previewActions, + previewHeight: params.previewHeight, + previewCommit: params.previewCommit, timestamp: Date.now() }); }