Skip to main content

iOS Navigation

iOS Navigation Architecture

Current 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.

  1. The UITabBarController that is effectively the root of the app, at least as far as navigation is concerned.
  2. The UINavigationControllers that exist within each of the tabs in the UITabBarController
  3. The Coordinator stacks that represent the views pushed onto each of the UINavigationControllers

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).
.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

Current Coordinator Architecture

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

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)

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))

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.

  1. It checks if the scheme of the URL matches one that we support
  2. Matches the URL against our deeplink templates to see if this URL request can actually be handled.
  3. If a matching template is found, the parameters (whether they are path, fragment or query parameters) are extracted, composing key value pairs.
  4. 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 Routes 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 Routes to Coordinators are as follows:

  1. If the Route is a Native only route. Return the relevant/associated Coordinator (implementing as pure native code)
  2. If the Route is associated with a feature flag, and that flag is disabled, default this Route to a ReactCoordinator or ModalReactCoordinator (defaults to using React Native screen essentially)
  3. If the Route has an available Coordinator that can handle the Route, return the relevant/associated Coordinator (implementing as pure native code)
  4. If none of the above rules apply, default this Route to a ReactCoordinator or ModalReactCoordinator (defaults to using React Native screen essentially)