Navigation
Navigating between screens is a core concept of building Hotwire Native apps. By default, all screens will be pushed onto the main navigation stack with animation. You can customize the navigation behavior by providing path configuration rules or manually routing in Swift or Kotlin.
﹟ Routing
Set context or presentation to a path configuration rule to apply the logic in the following table.
- State describes what state the app is currently in:
modalif a modal is presented,defaultotherwise. - Context is the value of the
contextproperty on the tapped link:modalordefault. No value defaults todefault. - Presentation is the value of the
presentationproperty on the tapped link:replace,pop,refresh,clear_all,replace_root,none, ordefault. No value defaults todefault.
| State | Context | Presentation | Behavior |
|---|---|---|---|
default |
default |
default |
Push on main stack (or) Replace if visiting same page (or) Pop then visit if previous screen is same URL |
default |
default |
replace |
Replace screen on main stack |
default |
modal |
default |
Present a modal with only this screen |
default |
modal |
replace |
Present a modal with only this screen |
modal |
default |
default |
Dismiss then Push on main stack |
modal |
default |
replace |
Dismiss then Replace on main stack |
modal |
modal |
default |
Push on the modal stack |
modal |
modal |
replace |
Replace screen on modal stack |
default |
(any) | pop |
Pop screen off main stack |
default |
(any) | refresh |
Pop on main stack then |
modal |
(any) | pop |
Pop screen off modal stack (or) Dismiss if one modal screen |
modal |
(any) | refresh |
Pop screen off modal stack then Refresh last screen on modal stack (or) Dismiss if one modal screen then Refresh last screen on main stack |
| (any) | (any) | clear_all |
Dismiss if modal screen then Pop to root then Refresh root screen on main stack |
| (any) | (any) | replace_root |
Dismiss if modal screen then Pop to root then Replace root screen on main stack |
| (any) | (any) | none |
Nothing |
﹟ Server-Driven Routing in Rails
If you’re using Ruby on Rails, the turbo-rails gem provides the following additional historical location routes. Use these to manipulate the navigation stack for Hotwire Native apps, falling back to redirecting elsewhere.
recede_or_redirect_to(url, **options)- First, pops any modal screen (if present) off the navigation stack. Then, pops the visible screen off of the navigation stack.refresh_or_redirect_to(url, **options)- First, pops any modal screen (if present) off the navigation stack. Then, reloads the visible screen by performing a new web request and invalidating the cache.resume_or_redirect_to(url **options)- Pops any modal screen (if present) off the navigation stack. No further action is taken.
The iOS and Android frameworks (starting in version 1.2.0) automatically support these historical location urls.
﹟ Route Decision Handlers
By default, all external urls outside of your app’s domain open externally. The specific behavior can be customized, though. Out-of-the-box, Hotwire Native registers these route decision handlers to control how urls are routed:
AppNavigationRouteDecisionHandler: Routes all internal urls on your app’s domain through your app.SafariViewControllerRouteDecisionHandler: (iOS Only) Routes all externalhttp/httpsurls to a SFSafariViewController in your app.BrowserTabRouteDecisionHandler: (Android Only) Routes all externalhttp/httpsurls to a Custom Tab in your app.SystemNavigationRouteDecisionHandler: Routes all remaining external urls (such assms:ormailto:) through device’s system navigation.
If you’d like to customize this behavior you can implement the RouteDecisionHandler protocol (iOS) or interface (Android) in your app to provide your own implementation(s). Register your app’s decision handlers in order of importance. To decide how a proposed visit should be routed, the registered RouteDecisionHandler instances are called in order. When a RouteDecisionHandler matching the proposal is found, its handle() function is called and no other RouteDecisionHandler instances will be subsequently called.
Example for iOS:
Hotwire.registerRouteDecisionHandlers([
AppNavigationRouteDecisionHandler(),
MyCustomExternalRouteDecisionHandler()
])
Example for Android:
Hotwire.registerRouteDecisionHandlers(
AppNavigationRouteDecisionHandler(),
MyCustomExternalRouteDecisionHandler()
)
Each decision handler receives the full VisitProposal, so you can match on the proposed visit’s location, visit options, or path configuration properties:
Example for iOS:
class MyCustomExternalRouteDecisionHandler: RouteDecisionHandler {
let name = "my-custom-external"
func matches(proposal: VisitProposal,
configuration: Navigator.Configuration) -> Bool {
proposal.url.host == "external.example.com"
}
func handle(proposal: VisitProposal,
configuration: Navigator.Configuration,
navigator: Navigating) -> Router.Decision {
// Route the url however you like, then cancel the in-app navigation.
return .cancel
}
}
Example for Android:
class MyCustomExternalRouteDecisionHandler : Router.RouteDecisionHandler {
override val name = "my-custom-external"
override fun matches(
proposal: VisitProposal,
configuration: NavigatorConfiguration
): Boolean {
return proposal.location.toUri().host == "external.example.com"
}
override fun handle(
proposal: VisitProposal,
configuration: NavigatorConfiguration,
activity: HotwireActivity
): Router.Decision {
// Route the url however you like, then cancel the in-app navigation.
return Router.Decision.CANCEL
}
}
﹟ Manual Navigation
Navigator can be used to navigate from a native screen to another native screen or back to a web context.
﹟ iOS
let rootURL = URL(string: "...")!
let navigator = Navigator()
// Visit a new page.
navigator.route(rootURL.appending(path: "foo"))
// Pop the top controller off the stack.
navigator.pop()
// Pop the entire stack of controllers.
navigator.clearAll()
Disable the animation via the optional animated parameter.
navigator.route(rootURL.appending(path: "foo"), animated: false)
navigator.pop(animated: false)
navigator.clearAll(animated: false)
﹟ Android
Inside of a HotwireActivity class:
val location = "https://..."
val navigator = delegate.currentNavigator
// Visit a new page.
navigator?.route("$location/foo")
// Pop the backstack to the previous destination.
navigator?.pop()
// Clear the navigation backstack to the start destination.
navigator?.clearAll()
Inside of a HotwireFragment class:
val location = "https://..."
// Visit a new page.
navigator.route("$location/foo")
// Pop the backstack to the previous destination.
navigator.pop()
// Clear the navigation backstack to the start destination.
navigator.clearAll()