00:00:15.400
Hello everyone! My name is Joe Masilotti, and this is just enough Turbo Native to be dangerous. Let's get started.
00:00:20.960
I'm here to tell you that Turbo Native gives Rails developers superpowers. It transforms our Rails codebases into iOS and Android apps by rendering our server-side HTML inside of native mobile apps—ones you can download from the app store and have full access to native SDKs like push notifications and geo-fencing.
00:00:32.520
But I get it; I was a hybrid skeptic too. I once worked for a company that had about a hundred Rails screens, and I was tasked to port that app to both iOS and Android. I was able to do so. I launched iOS and Android apps to both app stores as the only developer. This would have taken years if I had built out both platforms fully native. Instead, it took months. I had just picked up Turbo Native at the time. Imagine what you could do if you already know the framework.
00:00:54.560
Before this, I was an iOS developer. I built real apps with native SDKs. Thank you very much! But my world changed that day, and since then, I have been all in on Turbo Native. I write tutorials, create YouTube videos on the subject, and I'm even writing a book. I'm one of the two maintainers of the Turbo iOS library, and I've launched dozens of apps to the App Store and Google Play stores.
00:01:04.760
I live and breathe Turbo Native. But you’re probably saying that hybrid will never be as good as native, and you know what? You're right; it won't. Nothing will beat the absolute fluidity of gliding your finger across the screen with GPU-accelerated animations that would make Craig Federighi proud.
00:01:18.920
However, today I'm here to tell you that your web UI is good enough. Most screens today render data, and native controls won't help much. Turbo Native gives you a shortcut. It lets you launch into the App Store today with the resources you have instead of waiting months or years to deploy something and test your hypothesis. You can validate those resources now, and Turbo Native gets you there today.
00:01:41.480
And for those times when Turbo Native isn't good enough—when you want to go to higher fidelity—we have answers for that, and we'll talk about those a little bit later. But why choose Turbo Native over the myriad of other hybrid frameworks? Well, for Rails developers, it offers true write once, deploy anywhere.
00:01:54.680
You push your code with new changes to a mobile web view on your mobile screens, and your iOS and Android apps get those changes for free—no app store approval, no resubmitting, no rebuilding, no rebundling. If it's on the server, it's in your app. This allows you to maximize what you do best—writing Rails code. We’re not native developers; we don’t want to be native developers. We want to write Rails code.
00:02:09.240
Turbo Native enables this. It allows you to skip expensive development cycles of building out an API and then building it out on iOS and Android by writing it once and deploying it everywhere. When you really need to, you can upgrade specific screens to native, when you need access to native SDKs like the accelerometer, GPS, or push notifications.
00:02:14.040
But it lets you do it on a case-by-case basis when you're ready—it's not all or nothing. You can go web now, feature complete, and upgrade individual screens. These are just some of the apps that are in the App Store right now built with Turbo Native. Some of these might look familiar, as I've worked on about half of these.
00:02:42.440
Today, we're going to do four things: we're going to build an app from scratch, learn how to make it feel a little bit more native, learn how to integrate Swift UI screens to enhance fidelity, and finally, we'll preview some upcoming features for Turbo Native that I am very excited to share.
00:03:02.440
Today's talk is going to focus on iOS for the most part. I only have 30 minutes, but everything I talk about is applicable to both iOS and Android platforms. We're going to build the demo app, an iOS app that keeps track of your hiking, essentially functioning as a hiking journal app. We have an index page, a show page, and some structural elements in the output that we'll discuss later.
00:03:27.600
Now, this is the Rails server that's running locally. It’s a Rails app that I can click through. I can get a show page, I can edit; it’s a CRUD app. It's nothing too exciting, but we're going to build this into a Turbo Native iOS app. So, we'll open up Xcode and create a brand new project.
00:03:44.840
It's going to be an iOS app, and we're going to call it 'Demo.' We’re going to throw it on our desktop. This opens to the project explorer; we won't worry about this right now. We will focus entirely on the SceneDelegate.
00:04:00.440
This function happens when the app is launched. This is called 'sceneWillConnectTo.' We’re going to delete everything else in this file and only focus on this for now.
00:04:20.560
The first thing we’re going to do is create a navigation controller. A navigation controller is a basic building block for Turbo Native and on UIKit or what you use to build apps with on iOS. You’re probably familiar with it in the Contacts app, for example. This is a navigation controller; it’s a stack of screens where I can navigate forward and back, going deeper into multiple layers.
00:04:43.440
What we're going to do with that is set it as our window's root view controller. This means that when the app launches, we'll launch right into this navigation controller. Let's command-R to run it, and we’ll see an empty white screen, but that means it's working.
00:05:10.360
Next, we’re going to add the Turbo Native file package. Dependencies are going to hook us into the world of Swift Package Manager. It’s like gems for iOS. I'm going to add Turbo iOS from github.com/hotwired/turbo-ios. Then, we’ll add it to our demo target. If I pull open the explorer on the left—sorry this is a little small—but here is the code.
00:05:34.080
As if we were to do a bundle open, we can start logging into all of Turbo iOS right there. Once we have it, we’ll import it, giving us access to the great features of Turbo Native. One of the most critical ones is something called a session. A session manages the complex stuff; it manages HTML rendering, taking care of the screenshotting and snapshotting.
00:06:02.880
We'll create one and return it here using a lazy variable to only create it when we access it for the first time. Remember, this is when the app launches. When the app fires up, we want to visit our localhost.
00:06:32.760
Up at the top here, we’re going to create a global variable called rootURL; we’re going to point that to our localhost. Notice how it's outside of the class definition; this means we can access it across our app.
00:06:52.360
Down here, we're going to create a private function called 'visit.' This function is going to be called every time you want to make a visit to a new page in our app. The parameter it takes will be called 'proposal.' A visit proposal is how Turbo Native wraps every single visit to your screens.
00:07:18.760
We should command-click into this to see its definition. It has three properties: a URL, options, and properties. A URL is self-explanatory; it's the URL of the page you're visiting. Options are more advanced stuff for navigating, which is somewhat out of scope for today, and properties is a hash.
00:07:44.760
This hash will be important later when we start routing URLs. When we want to visit a page, we have to do three things: we want to create the screen, push the screen or display it onto our navigation stack, and finally, we want to visit the page or render the HTML.
00:08:06.520
So let's do those three things in three lines of code. First, we’ll create a visitable view controller. A visitable view controller is the core of Turbo Native rendering; this expects a URL that we can pull from the proposal. It manages our web view session that gets passed across the screen as we navigate deeper into our Turbo Native app.
00:08:31.440
It also manages snapshotting and caching. We can now push this visitable, passing in 'true' to animate it on every plus one screen to ensure we get that nice animation that makes it feel native. Finally, we take our session and visit the visitable. We’ll pass in the options from the visit proposal, ensuring that future visits will use all of the good stuff that that proposal already had.
00:09:04.040
Finally, up here, back in where our app launches, we’re going to create a proposal by passing in the URL of our root URL, which we defined earlier.
00:09:21.960
We’ll pass this off to 'visit,' and then command-R to run our app. If all goes well, we should see a spinner for a quick second and load our local host index page. We now have a Turbo Native app displaying our content with just 33 lines of code.
00:09:34.960
Now we have a problem: we can't click links. What's happening here is that the session is trying to tell our app to do something, but we haven't instructed it where to send those messages.
00:09:46.840
We're going to set up a delegate that says when a new visit is clicked, route it to me as the scene delegate. However, the scene delegate doesn’t conform; we need to tell Xcode we're strongly typed. Remember, we’re in Swift, not Ruby; we must tell Xcode that we listen to those methods.
00:10:09.680
Xcode will complain again, saying it understands, but you don't implement the three methods you're required to implement, so let's implement those now. 'Did propose visit' essentially happens when a link is clicked; we get that visit proposal as expected.
00:10:28.840
‘Did fail to visit' is what happens when an error occurs, and finally, 'Web view process did terminate' is an edge case when the web view dies outside of the context of our iOS app because it runs in a separate process. Let's start with that; it's easy as we can just reload the session.
00:10:56.560
This will ensure that our session reload can have a brand new context to work with and our page reloads. For 'did fail,' we will just print it out for now with the localized description of the error.
00:11:20.720
But this is the important one: when a link is clicked, we want to visit a page. Luckily, we have that function already right there; we can pass in 'visit' and pass in the proposal. We run this, and now we’re listening to that callback through the delegate, allowing navigation across our app.
00:11:45.440
We now have about 50 lines of code here for what would be the absolute bare minimum Turbo Native app that you could build off of a Rails app that’s running all the standards. Let's go back to the slides to discuss ways we can make this feel a little more native.
00:12:08.520
This is what we're working with, right? Zooming in on the top—we have a hiking journal app. The top middle is the page title; the top left is the back button. The title of that page is already 'Hiking Journal,' so we get it again, and then we have a nav bar rendered on the web. Let's deal with that one first.
00:12:31.440
We already have a native nav bar, so there's no need to have the web one as well. To do that, we’re going to render Turbo Native-specific content. However, we first have to identify the app as a Turbo Native app. To do that, we’ll set a user agent.
00:12:53.920
Here’s the session that we had before where we set the delegate. We’re going to create a web view configuration and pass in an application name for the user agent, calling it Turbo Native iOS. We’ll pass that into the session and configure it; with every request, we’ll get Turbo Native iOS appended to the user agent.
00:13:18.680
Then on Rails, if you’re using Turbo Rails, you get a helper for free. It’s buried deep in app controllers, and I’d recommend checking out that class. It checks for Turbo Native in the string and returns true.
00:13:39.440
To hide the nav bar in our app, we have our nav bar partial. Let's just render it unless it’s done hidden. Right? No problem, but that kind of sits wrong with me; maintaining a conditional every time you want to render Turbo Native content is going to complicate things.
00:13:50.960
We’re now sending different HTML over to different user agents. This means we need to include our user agent somehow in our cache key, which might double our cache size—no thanks.
00:14:12.280
Instead, we’re going to render Turbo Native-specific styles, ones that only apply to the Turbo Native app. We're going to create a new class called 'Turbo Native Hidden,' which is simply display: hidden; and important here.
00:14:38.800
This is a new stylesheet—native.css. This isn’t our application CSS; it's a new one, and we’ll include that in our application layout. Now, if you're rendering a Turbo Native app, this is the only conditional we need in our view layer. We’re sending down additional CSS with the same HTML, which overrides and customizes our base styles.
00:15:01.720
Fun things can happen, like changing how it looks on iOS or Android. We went from three hiking journals to two: one down and one to go. The key takeaway is that we rendered Turbo Native-specific content with only our Rails code. We deploy that change once—where we set the user agent—then we can change styles at will without doing App Store updates.
00:15:24.960
Let’s get rid of this one; the title is automatically set from the title HTML tag, buried somewhere in Turbo Native's visitable view controller. Our application layout now looks like this: we set the title to 'Hiking Journal.' We’re going to use a Rails feature called content_for.
00:15:46.760
It pulls the value from the title, and if not, defaults back to the static string in our show view for hikes. Here’s where we render out the content for; we’ve now set the title dynamically for every single page for each individual hike. This works great for your HTML as well, now you have an actual useful title displayed in the nav bar.
00:16:16.000
We can go one step further by hiding the H1 on Turbo Native apps, as we're already rendering it at the top. We don't need to render it again. Just like that, we now have a single 'Hiking Journal' displayed at the top left. While I understand these elements are very Bootstrap-like and might look ugly, we’ll clean them up later.
00:16:39.760
We’ve rendered native titles now with only Ruby code. We've deployed that change—the change that sets the user agent—and we can now do these native titles and change native elements using only our Ruby code.
00:17:05.040
Traditionally, a difficult part of native development is image uploads, which normally requires a lot of code. Here’s what we're going to do: step one, we’re going to add a file field backed by Active Storage in my app. You could back it by whatever you want in yours.
00:17:26.440
The file field is the important thing, and step two… that’s it! Turbon Native handles this for us. We're leveraging the fact that we’re a web view, which already has a ton of this functionality built-in. Image uploading is just one of those aspects.
00:17:51.760
We get date pickers, time pickers, and decimal inputs—this is common on iOS and Android. There are no new code changes needed, just the correct HTML markup and the right input fields. We should take advantage of the fact that Apple and Google have spent years perfecting these UIs.
00:18:11.960
Don't reinvent the wheel unless you absolutely need to. Your logic stays on the server, and you keep writing Ruby code—doing what you do best.
00:18:29.520
Now, let’s take a step further and discuss advanced Turbo Native screens. This is a local sandwich place in Portland that I recommend checking out if you're ever in town. As I was scrolling through their page, I noticed an embedded Google map.
00:18:46.640
You know how it goes: your finger gets stuck, you start scrolling the map, and accidentally click it. Then Google prompts you to use their app. This is a perfect example to upgrade to native on Turbo Native. Instead of rendering a screenshot of a map, we can use Swift UI to get a fully scrollable, zoomable Native interface.
00:19:12.920
So how do we do that? First, we need to determine when to route to this screen. This is the URL we will use; anything that says 'hikes/id/map' will want to trigger rendering the native map.
00:19:39.720
However, we need to remain flexible—we don’t want to hardcode this into our iOS app. If we ever wanted to render that map for, say, SL Maps ID, we’d need to redeploy to the App Store. The path configuration can help us keep this configuration on our server.
00:20:03.360
This is a server-hosted JSON file that looks like this. It has two keys: settings and rules. Today, we’ll discuss rules. These rules match when you click a URL, and you get this pattern that applies properties.
00:20:31.040
Here is the rule we're building, which is like a routes.rb for your iOS app. Every time we match this pattern, where 'hikes' is a number, we will apply these properties. Properties are then applied to the visit proposal.
00:20:59.960
This is a hash, and for those in the back who can’t see, I’m showing a visit proposal with properties rendered out, which has string keys. We will route this file in our Rails routing.
00:21:22.240
The configurations for iOS V1 will route to a controller. I like to version these to ensure we don't lose backward compatibility. If we ever add a new native feature, we want to ensure we don’t break old apps.
00:21:42.560
Here’s what that looks like copied over to an iOS controller. We have our session initialized with our root URL. Our session takes in a path configuration, and we can give it an array of sources, pointing to our server URL, appending our path.
00:22:00.920
As soon as the session is initialized lazily, Turbo Native will fetch, parse, and apply all these properties for us. It will also cache the path configuration for future launches, so you’re free to update this without worrying about doing any networking requests in the app—Turbo handles it all.
00:22:24.560
Now we need to actually show the screen. This is what we had before in our one-two-three method for visiting. We can throw all that in an else statement; that’s our standard web view process.
00:22:43.840
Our if statement will check whether the properties on the proposal have a string and whether the controller is named 'map.' This will be abstracted.
00:23:04.160
Every time we match this condition, we’ll render our map view controller. This is just one example of how configurable this is; you can imagine this if statement growing large, but each can be a different native feature routed from your server.
00:23:29.600
The path configuration allows us to keep our logic on the server, disconnecting us from App Store and Google Play releases. It also ensures backward compatibility by versioning, so old apps aren’t broken.
00:23:48.640
Now let’s talk about what's next for Turbo Native. I bet everyone knows what’s on the next slide: Strata! I’m very excited about this. Yesterday, Jay did an amazing job discussing it.
00:24:04.960
I won't dive too deeply, but at a high level, you can build native components with Strata—not full screens, but native components. It bridges the gap between web and native, ensuring that you keep your HTML powering your native components.
00:24:18.960
It's not some fancy JavaScript thing or a JSON endpoint; you write your HTML, markup it, and get native components from it. What's important for this presentation is that we can discard those ugly components.
00:24:35.920
With Strata, we can throw a button in the top right, an overflow button, and when you click that, we get the map and edit buttons with our little icons from SF Symbols.
00:24:59.760
This makes it so easy that it would take a lot of code to do before Strata, and the best part is that when I click “edit,” it triggers that action under the hood. Whatever is happening behind that button press, be it a new GET, a new POST request—it just works.
00:25:21.120
I'm also super excited about Turbo Navigator, a package I’ve been working on that simplifies getting started with Turbo iOS. As you saw, Turbo iOS is quick to get started, but anything beyond that requires a lot of boilerplate.
00:25:42.480
There are countless flows you have to manage. Turbo Navigator handles 15 of those for you. It can manage modal navigation, presenting something from the bottom of the screen and dismissing it.
00:26:05.680
It supports deep navigation into the stack, such as going one, two screens deep, and clearing everything, like when you log someone in or out. It also manages basic navigation of pushing and popping view controllers off the stack.
00:26:27.520
There are numerous other features, and I recommend checking out the GitHub for it. Turbo Navigator handles 15 different flows and minimizes about 100 lines of boilerplate. I use this for all my client apps, and the good news is it's being upstreamed into Turbo iOS very soon.
00:26:48.480
We finished about 90% of the work, and I promise it's coming soon. If you want an early look, check it out on GitHub, give it a watch, give it a star, and we’ll announce the upstream merge when it happens.
00:27:09.960
We talked a lot about building Turbo Native apps, but let’s discuss your Turbo Native apps. I help folks launch Turbo Native apps through coding, consulting, workshops, training, and advisory services.
00:27:23.840
I’d love to help you get yours into the App Store or Google Play Store. If you're interested in Turbo Native or want to discuss if it's right for your company, come say hi; I’d love to chat!
00:27:35.360
Again, I'm Joe Masilotti, and if you have any questions about this or Turbo Native, here’s my email. For more insights, I have a weekly newsletter and blog about Turbo Native regularly at masilotti.com.
00:27:37.430
Thank you!