This post was extracted and adapted from The Rails and Hotwire Codex. It also assumes some familiarity with Turbo Native.
When developing for the web, we can send the user to any location during a web request using an HTTP redirect (3xx
status codes). Turbo Native effectively wraps the website within native navigation. This means a redirect may not always do the trick.
Let's take an example. A Login screen is presented modally in the native apps. After a successful login, we want to redirect the user to the home page. In the app, we'd want to dismiss the modal to reveal the screen under it allowing the user to continue with whatever they were doing. A conventional redirect won't work, we need to tell the app to just dismiss the modal.
Server driven native navigation
The turbo-rails
gem draws three routes which instruct the native app to do something. These routes don't return any meaningful content; the app is meant to intercept visit proposals to these routes and implement logic to execute the instruction. The routes, and what they instruct, are:
Route | Instruction |
---|---|
/recede_historical_location |
The app should go back |
/resume_historical_location |
The app should do nothing in terms of navigation |
/refresh_historical_location |
The app should refresh the current page |
Check out the source code to see the routes and the actions they point to.
These routes are exclusively for the native apps and have no meaning on web. To help with this, turbo-rails
has methods to conditionally redirect a user if they're using the app. For example, instead of redirecting as below:
redirect_to root_path, status: :see_other
We would use:
recede_or_redirect_to root_path, status: :see_other
This will redirect to /
on the web, and to /recede_historical_location
in the native apps. Check out all the available redirect methods in the source code.
Using these methods, the app can be directed to dismiss the login modal after logging in while still redirecting to the root path on web.
Next, redirects to the aforementioned paths need to be intercepted and handled in the apps. We'll call these paths: path directives, since they direct the app to do something.
Intercept and handle path directives
Let's look at iOS. The following delegate method is triggered for every web request from the app.
extension RoutingController: SessionDelegate {
func session(_ session: Session, didProposeVisit proposal: VisitProposal) {
// ...
}
}
In here, we can check if a visit is a path directive and act accordingly.
extension RoutingController: SessionDelegate {
func session(_ session: Session, didProposeVisit proposal: VisitProposal) {
if proposal.isPathDirective {
executePathDirective(proposal)
} else {
visit(proposal)
}
}
func executePathDirective(_ proposal: VisitProposal) {
guard proposal.isPathDirective else { return }
switch proposal.url.path {
case "/recede_historical_location":
dismissOrPopViewController()
case "/refresh_historical_location":
refreshWebView()
default:
()
}
}
private func dismissOrPopViewController() {
// ...
}
private func refreshWebView() {
// ...
}
}
extension VisitProposal {
var isPathDirective: Bool {
return url.path.contains("_historical_location")
}
}
That'll do the trick on iOS.
Building a Turbo Native navigation system is non-trivial, so I've had to omit a lot of surrounding code to keep this post on point. My book fills in all the gaps if you're interested in digging deeper.
Next, let's look at Android. Custom navigation is handled by creating an interface
which inherits from TurboNavDestination
. In this interface, the shouldNavigateTo(newLocation: String)
method is called for every web request. We can handle path directives in here.
interface NavDestination: TurboNavDestination {
override fun shouldNavigateTo(newLocation: String): Boolean {
return when {
isPathDirective(newLocation) -> {
executePathDirective(newLocation)
false
}
else -> true
}
}
private fun executePathDirective(url: String) {
val url = URL(url)
when (url.path) {
"/recede_historical_location" -> navigateUp()
"/refresh_historical_location" -> refresh()
}
}
private fun isPathDirective(url: String): Boolean {
return url.contains("_historical_location")
}
// ...
}
Conclusion
That's how Turbo Native powered apps can be controller from the server! This gives us an immense amount of flexibility and extensibility without ever deploying app updates.
If you liked this post, check out my book, The Rails and Hotwire Codex, to level-up your Rails and Hotwire skills! It'll teach you how to build a Turbo Native navigation system and fills in the gaps in this blog post.
Top comments (2)
What does this look like in response payload? Isn't the web routing supposed to happen on the server ie checking user agent is chrome, returning status code 30x? If this is true, what does the native client receive? Same 30x but with those urls as destination? Will it be able to decode it? I know this post lists experience with turbo native as prerequisite but would you mind clearing that bit up?
Sure ... so the routing logic does happen on the server. The user agent is matched with the regex
/Turbo Native/
to determine whether or not a request is from a native app.On the web, it's a 303 redirect to
root_path
. On native, it's a 303 redirect to the path/recede_historical_location
. The logic to handle what to do when the app is redirected to this path lives in native code. That's where the behaviour of this redirect is handled. The body at this page doesn't contain anything useful, just plain text which says"receding..."
or something like that.Hope that clears things up.