[TOC]
React Native Navigation
App-wide support for 100% native navigation with an easy cross-platform interface. For iOS, this package is a wrapper around react-native-controllers, but provides a simplified more abstract API over it. This abstract API will be unified with the Android solution which is currently work in progress. It also fully supports redux if you use it.
Overview
Why use this package
One of the major things missing from React Native core is fully featured native navigation. Navigation includes the entire skeleton of your app with critical components like nav bars, tab bars and side menu drawers.
If you're trying to deliver a user experience that's on par with the best native apps out there, you simply can't compromise on JS-based components trying to fake the real thing.
For example, this package replaces the native NavigatorIOS that has been abandoned in favor of JS-based solutions that are easier to maintain. For more details see in-depth discussion here.
Installation - iOS
Note: We recommend using npm ver 3+. If you insist on using npm ver 2 please notice that the location for react-native-controllers in node_modules will be under the react-native-navigation folder and you'll need to change the paths in Xcode below accordingly.
-
Add the native files of the dependency react-native-controllers to your Xcode project:
-
In Xcode, in Project Navigator (left pane), right-click on the Libraries > Add files to [project name]. Add ./node_modules/react-native-controllers/ios/ReactNativeControllers.xcodeproj (screenshots)
-
In Xcode, in Project Navigator (left pane), click on your project (top) and select the Build Phases tab (right pane). In the Link Binary With Libraries section add libReactNativeControllers.a (screenshots)
-
In Xcode, in Project Navigator (left pane), click on your project (top) and select the Build Settings tab (right pane). In the Header Search Paths section add $(SRCROOT)/../node_modules/react-native-controllers/ios. Make sure on the right to mark this new path recursive (screenshots)
-
In Xcode, under your project files, modify AppDelegate.m according to this example
Installation - Android
Note: Android adaptation is still under active development therfore the API might break from time to time.
-
Add the following to your settings.gradle file located in the android folder:
include ':react-native-navigation'
project(':react-native-navigation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-navigation/android/app/')
-
Update project dependencies in build.gradle under app folder.
dependencies {
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+"
debugCompile project(path: ':react-native-navigation', configuration: 'libraryDebug')
releaseCompile project(path: ':react-native-navigation', configuration: 'libraryRelease')
}
3. Your `MainActivity` should extend `com.reactnativenavigation.controllers.SplashActivity` instead of `ReactActivity`. If you have any `react-native` related methods in your `MainActivity` you can safely delete them.
4. Create a custom Application class and update the `Application` element in `AndroidManifest.xml`
```java
import com.reactnativenavigation.NavigationApplication;
public class MyApplication extends NavigationApplication {
}
```
```xml
<application
android:name=".MyApplication"
...
/>
```
5. Implement `isDebug` and `createAdditionalReactPackages` methods
```java
import com.reactnativenavigation.NavigationApplication;
public class MyApplication extends NavigationApplication {
@Override
public boolean isDebug() {
// Make sure you are using BuildConfig from your own application
return BuildConfig.DEBUG;
}
@NonNull
@Override
public List<ReactPackage> createAdditionalReactPackages() {
// Add the packages you require here.
// No need to add RnnPackage and MainReactPackage
return null;
}
}
```
## Migrating to version 2.0
Migrating your code base to version 2.0 will require a few changes to your native Java code. The actual navigation API has not changed so there will be no changes to your JS code base.
* Your `MainActivity` should now extend `com.reactnativenavigation.controllers.SplashActivity`
* Delete the `getPackages()` from `MainActivity`. Don't forget to delete unused imports after this step.
* Create a custom Application class and update the `Application` element in `AndroidManifest.xml`
```java
import com.reactnativenavigation.NavigationApplication;
public class MyApplication extends NavigationApplication {
}
```
```xml
<application
android:name=".MyApplication"
...
/>
```
* Implement `isDebug` and `createAdditionalReactPackages`
```java
import com.reactnativenavigation.NavigationApplication;
public class MyApplication extends NavigationApplication {
@Override
public boolean isDebug() {
// Make sure you are using BuildConfig from your own application
return BuildConfig.DEBUG;
}
@NonNull
@Override
public List<ReactPackage> createAdditionalReactPackages() {
// Add the packages you require here.
// No need to add RnnPackage and MainReactPackage
return null;
}
}
```
## Usage
If you don't like reading, just jump into the fully working example projects:
* [example](example) - Example project showing the best practice use of this package. Shows many navigation features.
* [redux-example](example-redux) - Best practice use of this package in a [redux](https://github.com/reactjs/redux)-based project.
#### Step 1 - Change the way your app starts
This would normally go in your `index.ios.js`
```js
import { Navigation } from 'react-native-navigation';
import { registerScreens } from './screens';
registerScreens(); // this is where you register all of your app's screens
// start the app
Navigation.startTabBasedApp({
tabs: [
{
label: 'One',
screen: 'example.FirstTabScreen', // this is a registered name for a screen
icon: require('../img/one.png'),
selectedIcon: require('../img/one_selected.png'),
title: 'Screen One'
},
{
label: 'Two',
screen: 'example.SecondTabScreen',
icon: require('../img/two.png'),
selectedIcon: require('../img/two_selected.png'),
title: 'Screen Two'
}
]
});
Step 2 - Register all of your screen components
Every screen that you want to be able to place in a tab, push to the navigation stack or present modally needs to be registered. We recommend doing this in a central place, like screens/index.js.
Note: Since your screens will potentially be bundled with other packages, your registered name must be unique! Follow a namespacing convention like packageName.ScreenName.
import { Navigation } from 'react-native-navigation';
import FirstTabScreen from './FirstTabScreen';
import SecondTabScreen from './SecondTabScreen';
import PushedScreen from './PushedScreen';
export function registerScreens() {
Navigation.registerComponent('example.FirstTabScreen', () => FirstTabScreen);
Navigation.registerComponent('example.SecondTabScreen', () => SecondTabScreen);
Navigation.registerComponent('example.PushedScreen', () => PushedScreen);
}
Step 3 - That's it
If you want to do a navigation action like push a new screen over an existing one, take a look at the Screen API. It would look something like this:
this.props.navigator.push({
screen: 'example.PushedScreen',
title: 'Pushed Screen'
});
Top Level API
Navigation
import { Navigation } from 'react-native-navigation';
- registerComponent(screenID, generator, store = undefined, Provider = undefined)
Every screen component in your app must be registered with a unique name. The component itself is a traditional React component extending React.Component.
Navigation.registerComponent('example.FirstTabScreen', () => FirstTabScreen);
Navigation.registerComponent('example.FirstTabScreen', () => FirstTabScreen, store, Provider);
Change your app root into an app based on several tabs (usually 2-5), a very common pattern in iOS (like Facebook app or the iOS Contacts app). Every tab has its own navigation stack with a native nav bar.
Navigation.startTabBasedApp({
tabs: [
{
label: 'One',
screen: 'example.FirstTabScreen',
icon: require('../img/one.png'),
selectedIcon: require('../img/one_selected.png'),
title: 'Screen One',
navigatorStyle: {},
navigatorButtons: {}
},
{
label: 'Two',
screen: 'example.SecondTabScreen',
icon: require('../img/two.png'),
selectedIcon: require('../img/two_selected.png'),
title: 'Screen Two'
}
],
tabsStyle: {
tabBarButtonColor: '#ffff00',
tabBarSelectedButtonColor: '#ff9900',
tabBarBackgroundColor: '#551A8B'
},
drawer: {
left: {
screen: 'example.FirstSideMenu'
},
right: {
screen: 'example.SecondSideMenu'
},
disableOpenGesture: false
},
passProps: {},
animationType: 'slide-down'
});
- startSingleScreenApp(params)
Change your app root into an app based on a single screen (like the iOS Calendar or Settings app). The screen will receive its own navigation stack with a native nav bar
Navigation.startSingleScreenApp({
screen: {
screen: 'example.WelcomeScreen',
title: 'Welcome',
navigatorStyle: {},
navigatorButtons: {}
},
drawer: {
left: {
screen: 'example.FirstSideMenu'
},
right: {
screen: 'example.SecondSideMenu'
},
disableOpenGesture: false
},
passProps: {},
animationType: 'slide-down'
});
Show a screen as a modal.
Navigation.showModal({
screen: "example.ModalScreen",
title: "Modal",
passProps: {},
navigatorStyle: {},
navigatorButtons: {},
animationType: 'slide-up'
});
- dismissModal(params = {})
Dismiss the current modal.
Navigation.dismissModal({
animationType: 'slide-down'
});
- dismissAllModals(params = {})
Dismiss all the current modals at the same time.
Navigation.dismissAllModals({
animationType: 'slide-down'
});
- showLightBox(params = {})
Show a screen as a lightbox.
Navigation.showLightBox({
screen: "example.LightBoxScreen",
passProps: {},
style: {
backgroundBlur: "dark",
backgroundColor: "#ff000080"
}
});
- dismissLightBox(params = {})
Dismiss the current lightbox.
Navigation.dismissLightBox();
- registerScreen(screenID, generator)
This is an internal function you probably don't want to use directly. If your screen components extend Screen directly (import { Screen } from 'react-native-navigation'), you can register them directly with registerScreen instead of with registerComponent. The main benefit of using registerComponent is that it wraps your regular screen component with a Screen automatically.
Navigation.registerScreen('example.AdvancedScreen', () => AdvancedScreen);
Screen API
This API is relevant when in a screen component context - it allows a screen to push other screens, pop screens, change its navigator style, etc. Access to this API is available through the navigator object that is passed to your component through props.
Push a new screen into this screen's navigation stack.
this.props.navigator.push({
screen: 'example.ScreenThree',
title: undefined,
titleImage: require('../../img/my_image.png'),
passProps: {},
animated: true,
backButtonTitle: undefined,
backButtonHidden: false,
navigatorStyle: {},
navigatorButtons: {}
});
Pop the top screen from this screen's navigation stack.
this.props.navigator.pop({
animated: true
});
Pop all the screens until the root from this screen's navigation stack.
this.props.navigator.popToRoot({
animated: true
});
Reset the screen's navigation stack to a new screen (the stack root is changed).
this.props.navigator.resetTo({
screen: 'example.ScreenThree',
title: undefined,
passProps: {},
animated: true,
navigatorStyle: {},
navigatorButtons: {}
});
Show a screen as a modal.
this.props.navigator.showModal({
screen: "example.ModalScreen",
title: "Modal",
passProps: {},
navigatorStyle: {},
animationType: 'slide-up'
});
- dismissModal(params = {})
Dismiss the current modal.
this.props.navigator.dismissModal({
animationType: 'slide-down'
});
- dismissAllModals(params = {})
Dismiss all the current modals at the same time.
this.props.navigator.dismissAllModals({
animationType: 'slide-down'
});
- showLightBox(params = {})
Show a screen as a lightbox.
this.props.navigator.showLightBox({
screen: "example.LightBoxScreen",
passProps: {},
style: {
backgroundBlur: "dark",
backgroundColor: "#ff000080"
}
});
- dismissLightBox(params = {})
Dismiss the current lightbox.
this.props.navigator.dismissLightBox();
- handleDeepLink(params = {})
Trigger a deep link within the app. See deep links for more details about how screens can listen for deep link events.
this.props.navigator.handleDeepLink({
link: "chats/2349823023"
});
- setOnNavigatorEvent(callback)
Set a handler for navigator events (like nav button press). This would normally go in your component constructor.
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
Set buttons dynamically on the navigator. If your buttons don't change during runtime, see "Adding buttons to the navigator" below to add them using static navigatorButtons = {...};.
this.props.navigator.setButtons({
leftButtons: [],
rightButtons: [],
animated: true
});
Set the nav bar title dynamically. If your title doesn't change during runtime, set it when the screen is defined / pushed.
this.props.navigator.setTitle({
title: "Dynamic Title"
});
- toggleDrawer(params = {})
Toggle the side menu drawer assuming you have one in your app.
this.props.navigator.toggleDrawer({
side: 'left',
animated: true,
to: 'open'
});
Toggle whether the tabs are displayed or not (only in tab-based apps).
this.props.navigator.toggleTabs({
to: 'hidden',
animated: true
});
Set the badge on a tab (any string or numeric value).
this.props.navigator.setTabBadge({
tabIndex: 0,
badge: 17
});
Switch to a tab (sets it as the currently selected tab).
this.props.navigator.switchToTab({
tabIndex: 0
});
- toggleNavBar(params = {})
Toggle whether the navigation bar is displayed or not.
this.props.navigator.toggleNavBar({
to: 'hidden',
animated: true
});
Styling the navigator
You can style the navigator appearance and behavior by passing a navigatorStyle object. This object can be passed when the screen is originally created; can be defined per-screen by setting static navigatorStyle = {}; on the screen component; and can be overridden when a screen is pushed.
The easiest way to style your screen is by adding static navigatorStyle = {}; to your screen React component definition.
export default class StyledScreen extends Component {
static navigatorStyle = {
drawUnderNavBar: true,
navBarTranslucent: true
};
constructor(props) {
super(props);
}
render() {
return (
<View style={{flex: 1}}>...</View>
);
}
Style object format
{
navBarTextColor: '#000000',
navBarBackgroundColor: '#f7f7f7',
navBarButtonColor: '#007aff',
navBarHidden: false,
navBarHideOnScroll: false,
navBarTranslucent: false,
navBarTransparent: false,
navBarNoBorder: false,
drawUnderNavBar: false,
drawUnderTabBar: false,
statusBarBlur: false,
navBarBlur: false,
tabBarHidden: false,
statusBarHideWithNavBar: false
statusBarHidden: false,
statusBarTextColorScheme: 'dark'
}
Note: If you set any styles related to the Status Bar, make sure that in Xcode > project > Info.plist, the property View controller-based status bar appearance is set to YES.
All supported styles are defined here. There's also an example project there showcasing all the different styles.
Adding buttons to the navigator
Nav bar buttons can be defined per-screen by adding static navigatorButtons = {...}; on the screen component definition. This object can also be passed when the screen is originally created; and can be overridden when a screen is pushed. Handle onPress events for the buttons by setting your handler with navigator.setOnNavigatorEvent(callback).
class FirstTabScreen extends Component {
static navigatorButtons = {
rightButtons: [
{
title: 'Edit',
id: 'edit',
testID: 'e2e_rules',
disabled: true,
disableIconTint: true,
showAsAction: 'ifRoom'
},
{
icon: require('../../img/navicon_add.png'),
id: 'add'
}
]
};
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
if (event.type == 'NavBarButtonPress') {
if (event.id == 'edit') {
AlertIOS.alert('NavBar', 'Edit button pressed');
}
if (event.id == 'add') {
AlertIOS.alert('NavBar', 'Add button pressed');
}
}
}
render() {
return (
<View style={{flex: 1}}>...</View>
);
}
Buttons object format
{
rightButtons: [{
title: 'Edit',
icon: require('../../img/navicon_edit.png'),
id: 'compose',
testID: 'e2e_is_awesome',
disabled: true,
disableIconTint: true,
}],
leftButtons: []
}
Styling the tab bar
You can style the tab bar appearance by passing a tabsStyle object when the app is originally created (on startTabBasedApp).
Navigation.startTabBasedApp({
tabs: [ ... ],
tabsStyle: {
tabBarButtonColor: '#ff0000'
}
});
Style object format
{
tabBarButtonColor: '#ffff00',
tabBarSelectedButtonColor: '#ff9900',
tabBarBackgroundColor: '#551A8B'
}
All supported styles are defined here. There's also an example project there showcasing all the different styles.
Deep links
Deep links are strings which represent internal app paths/routes. They commonly appear on push notification payloads to control which section of the app should be displayed when the notification is clicked. For example, in a chat app, clicking on the notification should open the relevant conversation on the "chats" tab.
Another use-case for deep links is when one screen wants to control what happens in another sibling screen. Normally, a screen can only push/pop from its own stack, it cannot access the navigation stack of a sibling tab for example. Returning to our chat app example, assume that by clicking on a contact in the "contacts" tab we want to open the relevant conversation in the "chats" tab. Since the tabs are siblings, you can achieve this behavior by triggering a deep link:
onContactSelected(contactID) {
this.props.navigator.handleDeepLink({
link: 'chats/' + contactID
});
}
Tip: Deep links are also the recommended way to handle side drawer actions. Since the side drawer screen is a sibling to the rest of the app, it can control the other screens by triggering deep links.
Handling deep links
Every deep link event is broadcasted to all screens. A screen can listen to these events by defining a handler using setOnNavigatorEvent (much like listening for button events). Using this handler, the screen can filter links directed to it by parsing the link string and act upon any relevant links found.
export default class SecondTabScreen extends Component {
constructor(props) {
super(props);
this.props.navigator.setOnNavigatorEvent(this.onNavigatorEvent.bind(this));
}
onNavigatorEvent(event) {
if (event.type == 'DeepLink') {
const parts = event.link.split('/');
if (parts[0] == 'tab2') {
}
}
}
Deep link string format
There is no specification for the format of deep links. Since you're implementing the parsing logic in your handlers, you can use any format you wish.
Third party libraries support
react-native-vector-icons
If you would like to use react-native-vector-icons for your Toolbar icons, you can follow this example or this gist.
License
The MIT License.
See LICENSE