iOS Navigation
iOS Navigation Architecture
Overview
As it currently stands, the Navigation Architecture for iOS centers primarily around the NativeAppView
and the Router
as the primary drivers for navigation. Since navigation must be synchronised between native and RN, special care needs to be taken when handling navigation events coming from either side, which is what this architecture endeavours to do.
NativeAppView
The NativeAppView
is responsible for receiving requests from RN in order for navigation to be driven by native, but also handles the synchronisation of navigation state currently within the native app, back to RN. Thus NativeAppView
acts as the bridge between native and RN, and any navigation related communication between the two generally occurs here (with some exceptions).
Additionally, the NativeAppView
serves as the root point of the native application itself, as the app is actually embedded inside a RN window, therefore some of the app initialisation will also occur here as well as the AppDelegate
or SceneDelegate
.
More of this class will be covered in the bridge section of the documentation.
Router
The Router
is the other primary class that drives navigation for native, and is responsible for funnelling navigation actions from both native and RN in order to process those events in a consistent manner across both platforms.
Currently the Router
serves as both the service for routing, and also contains the navigation state as it currently exists. This will be altered in the future so that the navigation state lives separately.
The navigation state is contained in the parts.
- The
UITabBarController
that is effectively the root of the app, at least as far as navigation is concerned. - The
UINavigationController
s that exist within each of the tabs in theUITabBarController
- The
Coordinator
stacks that represent the views pushed onto each of theUINavigationController
s
The core function that will be invoked when using the router is the perform(action: NavigationAction).
This allows specific navigation events to be performed on the current navigation state. Each of the actions will funnel through to a private function that the Router
implements, where the specific functionality of that action is set. When it comes to the navigation events, there are both a combination of simple navigation events one would expect from a typical iOS application such as .push or .pop, but also contain some contextual events specific to the RN application, such as .setParams and .navigate.
The navigation events that are supported by the NavigationAction type are:
.goBack
This event from RN signals that the user is attempting to go back via the back arrow in the top left corner of the navigation bar..setParams(params: NSDictionary?)
This event from RN signals that the parameters of the current screen should be updated (this means that the new parameters should be merged with the existing parameters, and ensuring that any competing keys use the newly updated parameter values)..navigate(action: Action?, route: Route?, stack: String?)
This event is the primary navigation event that the app will receive from RN, and signals to the native app to route to the specified screen. This navigation action could also require complex navigation events such as popping or dismissing before any pushing is done. This is currently handled by RoutingRules..reset
This event from RN signals that the app navigation state should reset to the initial state..resetHome
This event from RN signals the same as the reset case, but also states that the user should be set to the Home tab of the application..openDrawer(drawerState: DrawerState?)
This event from RN signals that a drawer should be opened..closerDrawer
This event from RN signals that the currently open drawer (if there is one) should be closed..toggleDrawer(drawerState: DrawerState?)
This event from RN signals that the state of the drawer should be toggled open/closed..push(navParams: NavigateParams?)
This event from RN signals that we should push a screen onto the navigation stack using the parameters..pop(popParams: PopParams?)
This event from RN signals that we should perform a pop operation on the navigation stack using the parameters..replace(navParams: NavigateParams?)
This event from RN signals that a screen on the stack should be replaced by another screen..popToTop
This event from RN signals that the current stack should pop to the top screen (root most screen).
Navigation Actions
.goBack
The .goBack case of a NavigationAction is a signal from RN that the user is attempting to go back via the back arrow in the top left corner of the navigation bar.
Coordinator
The Coordinator
(implementing the protocol CoordinatorWithRouting
) is the second point in which we need to inspect when utilising the navigation. The Coordinator
is used to control a screen, or a flow of screens within navigation.
Currently there are two types that implement this protocol, the ReactCoordinator
which allows for the navigating to RN screens, and the ModalReactCoordinator
which does the same, but in a modal context. Both of these inherit the NavigationCoordinator
superclass, which is intended to be an abstract class that provides most of the boilerplate code that a coordinator needs in order to function within the Router
. It is advised that most, if not all, coordinators inherit NavigationCoordinator
to make the usage and flow of coordinators consistent, and override any functions that require special treatment. It is expected that the CoordinatorWithRouting
and NavigationCoordinator
may evolve over time.
As mentioned earlier, our coordinators employ a linked-list approach to the stack, which allows each parent to be aware of its children, and visa versa. Within the CoordinatorWithRouting
there exists a series of properties and getters that allow us to quickly navigate through an individual stack of coordinators, for example, using the youngestChild() computed property to get the youngest child in the stack (the most recently created coordinator). Each coordinator has an associated Route
that allows the `Router to determine which coordinator to use upon a Route
** being provided, and it is also used to generate the EntainScreen (in the case of a RN screen needing to be rendered) once the appropriate view controller is instantiated.
The other methods for the CoordinatorWithRouting
, are start() and stop() which are used to start and stop the coordinator.
Below demonstrates the relationship between the Coordinators, Router and the various subclass implementations of Coordinators
Updates to the coordinator system now allow the RN functionality to synchronise with the bridge almost completely without needing to be exposed to the rest of the navigation architecture. This allows natively implemented coordinators to function in a more native manner.
Usage Example
Navigating from React Native to Native
In order to perform this type of navigation, a regular navigation action has to be generated and passed through the bridge so that the Native app can pick up the navigation action, parse it, and perform the required navigation. The only requirement here is that the route being navigated to, is supported by the Native application. This will require the following:
A coordinator that handles this route to be implemented in Native In the Route+CoordinatorMappings file, under the getCoordinator() method, the code needs to detect if a route is supported by native, and if it is, instantiate and return the native coordinator, rather than simply returning (by default) a React Native coordinator (which contains an EntainScreen)
Navigating From Native to React
In order to navigate to a ReactNative screen from Native you will need to do the following:
Generate the desired route that should be navigated to, for example:
// Just by screen name
let route = Route.ScreenName(rawValue: "ReactNativeScreenName")
// With parameters
let params = ["key", "value"] as! NSDictionary
let route = Route(key: "", name: title, params: params)
Using the generated route above, use the Router to perform a navigation action to that route, for example:
router.perform(action: .navigate(nil, route: route, stack: nil))
Navigating to a Modal Screen
In order to navigate to a modal screen, you must do the following:
The screen name must be flagged as being a modal
if let screenName = Route.ScreenName(rawValue: screenNameString)
// The screen name must be registered as a modal in the constants, for it to be picked up as a modal screen for React Native
screenName.isModalScreen
// This registration can be done in Route+ScreenName
This will automatically make React Native screen appear in modals if it is flagged as such
/// Router extension
if route.isModalScreen(routeConfigService: routeConfigService) {
return coordinatorFactory.makeModalReactCoordinator(router: self, navigationBridge: bridge)
}
return coordinatorFactory.makeReactCoordinator(router: self, navigationBridge: bridge)
Native Coordinators will have to determine their behaviour (whether modal or non-modal) internally.
DeeplinkRouter
The DeeplinkRouter is the first point of call for any deeplinks that are sent to the app, either from an external URL source such as a browser or other application, or from within the app itself, by requesting to open a URL (that happens to be a deeplink formatted URL). The DeeplinkRouter takes the deeplink URL passed in and does a few things.
- It checks if the scheme of the URL matches one that we support
- Matches the URL against our deeplink templates to see if this URL request can actually be handled.
- If a matching template is found, the parameters (whether they are path, fragment or query parameters) are extracted, composing key value pairs.
- Finally, a Route is generated with the parameters from step 3 loaded in, and sent as a navigation action to the Router, allowing the app to flow to required screen, passing in required parameters. More information on Deeplinking can be found here: Native Deeplink Documentation
More information on Deeplinking discovery from ReactNative can be found here: Native Deeplinking Parameters Documentation
RouterMappings
The RouterMappings class is responsible for handling the mapping between Route
s and screen names, to Coordinators. At times special rules may be invoked to branch between which Coordinator gets instantiated when a Route
is requested to be navigated to.
The rules for mapping Route
s to Coordinators are as follows:
- If the
Route
is a Native only route. Return the relevant/associated Coordinator (implementing as pure native code) - If the
Route
is associated with a feature flag, and that flag is disabled, default thisRoute
to aReactCoordinator
orModalReactCoordinator
(defaults to using React Native screen essentially) - If the
Route
has an available Coordinator that can handle theRoute
, return the relevant/associated Coordinator (implementing as pure native code) - If none of the above rules apply, default this Route to a
ReactCoordinator
orModalReactCoordinator
(defaults to using React Native screen essentially)