Menu

Navigation and Stacking

This document disects the internals of how Astro implements navigation and stacking in the NavigationPlugin. By the end of this document you'll understand how the navigation plugin works, how it manages the "back stack", and how back navigations work. This document covers both "classic" navigations and single-page app navigations.

Overview

Astro always stacks web views when navigating to a new url (address). This is implemented within the navigationPlugin.js. The NavigationPlugin creates a new WebViewPlugin, calls navigate on it with the requested URL, and then pushes it on the navigation stack by calling navigateToPlugin(webViewPlugin) on itself.

Astro detects when a WebView is trying to navigate to a new URL and rejects the navigation, instead triggering an event on the WebViewPlugin as a navigate event. Typically, in app.js, the event is handled and driven back to the NavigationPlugin through a navigateToUrl(url) call resulting in a new WebViewPlugin being pushed onto the stack (leaving the previous WebView at the "pre navigation" state enabling "fast back" navigation).

When a PWA is hosted inside Astro, the web view never navigates in the classic sense. Instead, the DOM is modified in-place without a navigation to a new URL. This presents a challenge as the hosting WebView does not get notified automatically that any transition is occurring.

Progressive Web Apps

Astro supports hosting Progressive Web Apps (PWA) which are basically single page apps (SPA) as implemented in the Mobify platform. The PWA has special logic to detect that it is in an Astro app. By coordinating with Astro we can achieve native-like navigations, reduce memory usage, and maintain fast back navigations.

PWA Coordination

Mobify PWA's have been made "app aware" (see [PR

#214](https://github.com/mobify/progressive-web-scaffold/pull/214)). When a PWA is about to transition to a new state ("navigate"), it triggers a custom custom event to notify Astro that the web view is about to transition. This event (pwa-navigate) is special in that it never reaches app.js; it is intercepted on the native side and handled specially. Because Astro handles back navigation by simply "unstacking" (popping the current webview off of the navigation stack and throwing it away) the PWA doesn't raise a pwa-navigate event when it needs to navigate back. Instead, Astro instructs the PWA to navigate back whenever the NavigationPlugin does a back navigation. See below for more details about how this back navigation works.

Astro Behaviour

When the Astro intercepts a pwa-navigate event, it stacks a new header item and prevents the event from propagating up to app.js. It then puts a placeholder in place of the navigating webview and re-stacks the webview as the new navigation item.

Imagine a loaded WebViewPlugin with a loaded PWA (*P hosts a PWA, *C is a classic):

 ---> TOP
+------+
|      |
|      |
|  W1  |
|      |
|      |
+------+

After a transition (pwa navigation) it would look like this:

 ---> TOP
+------+ +------+
|      | |      |
|      | |      |
|  P1  | |  W1  |
|      | |  *P  |
|      | |      |
|      | |      |
+------+ +------+

Now if we hit a link that navigates away from the PWA ("classic" navigation) it would look like this:

 ---> TOP
+------+ +------+ +------+
|      | |      | |      |
|      | |      | |      |
|  P1  | |  W1  | |  W2  |
|      | |  *P  | |  *C  |
|      | |      | |      |
+------+ +------+ +------+

If we go back to the PWA next, we'll see a new WebViePlugin stack which will re-start the placeholder stacking from that point on. After two more pwa navigations the navigation stack would look like this:

+------+ +------+ +------+ +------+ +------+ +------+
|      | |      | |      | |      | |      | |      |
|      | |      | |      | |      | |      | |      |
|  P1  | |  W1  | |  W2  | |  P2  | |  P3  | |  W3  |
|      | |  *P  | |  *C  | |      | |      | |  *P  |
|      | |      | |      | |      | |      | |      |
+------+ +------+ +------+ +------+ +------+ +------+

Platform Specific Behaviour

The iOS and Android implementations are very similar, but there are some differences outlined below.

iOS

On iOS, the NavigationPlugin (through the UINavigationController) provides an "interactive pop gesture". Meaning you can start swiping from the right edge to pop the current navigation item (ie. go back).

iOS interactive pop

In a PWA navigating doesn't take the browser to a new URL/page. It simply transitions the DOM to a new state. This results in Astro never stacking any web views after loading a PWA. With the pwa-navigate event we can stack a new web view, but we end up navigating to the exact same URL (possibly with a differnet fragment) but rendering a different UI. This means we basically have a whole stack of web views that have loaded the same PWA but are in differnet visual states.

Astro avoids this by only using one web view and taking snapshots right before the PWA "navigates" (changes the DOM). This is done by adding a gesture recognizer to web view and taking a snapshot of the view each time the user interacts with the web view. This means we take more snapshots than necessary, but so far this doesn't seem to affect performance.

When the WebViewPlugin sees the pwa-navigate event coming out of its web view, it notifies the NavigationPlugin. The NavigationPlugin then asks the WebView for it's latest snapshot and creates a placeholder view controller from that. Finally it replaces the current web view view controller on the stack with the placeholder and "re-stacks" the web view.

Android

Android works in a similar fashion to the iOS implementation.

Back navigation

Back navigation must also understand PWA navigations. The "back stack" of a NavigationPlugin that has visited a PWA will contain a mixture of placeholders and actual WebViewPlugins (and possibly other custom plugins). The top of the stack will always be some type of actual plugin (typically a WebViewPlugin). Behind any WebViewPlugin there could be one, or more, placeholders.

If we start going back you can see how WebViewPlugins keep slipping back up the stack until there are no more placeholders behind them at which point they are popped just as they always were.

+------+ +------+ +------+ +------+ +------+ +------+
|      | |      | |      | |      | |      | |      |
|      | |      | |      | |      | |      | |      |
|  P1  | |  W1  | |  W2  | |  P2  | |  P3  | |  W3  |
|      | |  *P  | |  *C  | |  *P  | |  *P  | |  *P  |
|      | |      | |      | |      | |      | |      |
+------+ +------+ +------+ +------+ +------+ +------+
← Back
+------+ +------+ +------+ +------+ +------+
|      | |      | |      | |      | |      |
|      | |      | |      | |      | |      |
|  P1  | |  W1  | |  W2  | |  P2  | |  W3  |
|      | |  *P  | |  *C  | |  *P  | |  *P  |
|      | |      | |      | |      | |      |
+------+ +------+ +------+ +------+ +------+
← Back
+------+ +------+ +------+ +------+
|      | |      | |      | |      |
|      | |      | |      | |      |
|  P1  | |  W1  | |  W2  | |  W3  |
|      | |  *P  | |  *C  | |  *P  |
|      | |      | |      | |      |
+------+ +------+ +------+ +------+
← Back
+------+ +------+ +------+
|      | |      | |      |
|      | |      | |      |
|  P1  | |  W1  | |  W2  |
|      | |  *P  | |  *C  |
|      | |      | |      |
+------+ +------+ +------+
← Back
+------+ +------+
|      | |      |
|      | |      |
|  P1  | |  W1  |
|      | |  *P  |
|      | |      |
+------+ +------+
← Back
+------+
|      |
|      |
|  W1  |
|  *P  |
|      |
+------+

The algorithm for a back navigation is as follows: