Workshop: Hotwire Native - A Rails developer’s secret tool to building mobile apps


Summarized using AI

Workshop: Hotwire Native - A Rails developer’s secret tool to building mobile apps

Joe Masilotti • July 09, 2025 • Philadelphia, PA • Workshop

Workshop Overview

This workshop, led by Joe Masilotti at RailsConf 2025, introduces Hotwire Native as a powerful tool enabling Ruby on Rails developers to build native iOS and Android apps without extensive knowledge of Swift or Kotlin. Instead, developers reuse their existing web screens (HTML and CSS) across web, iOS, and Android platforms, reducing development costs and time.

Main Topics Covered

  • Introduction to Hotwire Native:

    • Allows reuse of Rails web screens for native iOS and Android apps.
    • Eliminates the need for parallel development teams for each platform.
    • Enables instant updates across platforms without App Store review, as updates are served from the Rails backend.
    • Powers production apps like Basecamp and Hey, serving millions of users.
  • Live Coding: Building iOS and Android Apps:

    • Guided attendees through creating iOS (with Xcode) and Android (with Android Studio) apps from scratch.
    • Introduced Swift Package Manager (iOS) and Gradle (Android) for dependency management.
    • Demonstrated integrating the Hotwire Native library in both environments.
    • Covered nuances such as localhost networking differences between emulators/simulators.
  • Hands-on Application Setup:

    • Attendees cloned a demo Rails blog app as the backend.
    • iOS and Android frontends connected to the local Rails server, instantly rendering web-powered screens in a native shell.
    • Highlighted the benefits of Hotwire Native’s architecture (e.g., single WebView, Turbo.js for page speed, native navigation stacks).
  • Native Look and Feel:

    • Techniques to hide web UI elements (e.g., headers, navbars) on native apps using custom CSS (including Tailwind tips).
    • Leveraged native navigation bars and titles by dynamically syncing with the page <title> tag.
  • Adding Native Features:

    • Native Tab Bars: Implemented platform-native tab bars for improved navigation, demonstrating how to add dynamic tabs and icons using SF Symbols or Android equivalents.
    • Discussed advanced tricks, such as dynamically generating tabs from a JSON endpoint, enhancing flexibility.
  • Bridge Components:

    • Explained and demonstrated bridge components—stimulus-based wrappers to bridge web code (JavaScript/HTML) and native APIs.
    • Walked through building a custom bridge component to trigger a native confirmation dialog from web code, passing data fluidly between front-end and platform-native code.
    • Provided troubleshooting and debugging methods for both platforms, utilizing tools like Safari and Chrome DevTools for in-app debugging.
  • Best Practices and Takeaways:

    • Encourage minimal native code—leverage Rails business logic and update frequency.
    • When advanced platform integration is needed (e.g., notifications, document scanning), use bridge components for granular control.
    • Emphasized keeping bridge components generic (e.g., alert, confirm, barcode scanner) rather than tied to app-specific models.
    • Shared resources for further exploration: a library of prebuilt bridge components and Masilotti's Hotwire Native book.

Main Takeaways

  • Hotwire Native shifts the paradigm for Rails developers, letting them ship and maintain production-grade mobile apps on both iOS and Android with significant code reuse and minimal platform-specific code.
  • The framework supports progressive native enhancements—starting with shared HTML/CSS and adding native functionality when needed, reducing ongoing maintenance and enabling rapid iteration on all platforms.
  • Bridge components provide a powerful extension mechanism, unlocking native APIs as business needs evolve, all while keeping most application logic and UI within familiar Rails patterns.

Workshop: Hotwire Native - A Rails developer’s secret tool to building mobile apps
Joe Masilotti • Philadelphia, PA • Workshop

Date: July 09, 2025
Published: July 23, 2025
Announced: unknown

Building native mobile apps is time-consuming and expensive. Each screen must be built three times: once for web, again for iOS, and a third time for Android.

But with Hotwire Native you only need to build your screens once, in HTML and CSS, and then reuse them across all three platforms. If you already have a Hotwire-enabled Rails app, you can use the screens you've already built!

And you don't need to be an expert in Swift or Kotlin. A thin wrapper for each platform enables continuous updates by only making changes to your Rails codebase. Deploy your code and all three platforms get your changes immediately.

Join me as I build iOS and Android apps from scratch, live. Learn the essentials, practical tips, and common pitfalls I’ve picked up since working with Hotwire Native since 2016.

RailsConf 2025

00:00:17.440 Hello everyone. Close Slack. Um, okay. First
00:00:22.880 housekeeping bits. Who has the hard drive? Raise it up in the air or raise your hand if you have a hard drive.
00:00:28.560 Okay, so they have the hard drive with Android Studio and Xcode on it. If you need it and you don't want to use the
00:00:35.280 download speeds of the hotel, just grab that and you can swap it over to your computer. Uh, it's the fastest read
00:00:42.239 speed hard drive I could find on Amazon in 24 hours. So, good luck. Um, okay,
00:00:48.239 let's do this. Okay, so, uh, today we're going to be
00:00:53.440 talking about Hotwire Native. Uh, we're going to be building Hotwire Native apps. Uh,
00:00:59.120 Hotwire Native is what I like to call a Rails developer secret tool to building mobile apps. Uh, you can build iOS and
00:01:06.960 Android apps powered by Hotwire Native from scratch. We're going to do that today. Uh, you won't need any Swift or
00:01:12.720 Cotlin experience to get started. We're going to be walking through everything.
00:01:17.759 Um, if you want a link to these slides, there's a QR code right there. Uh, you could also drop to masalotti.com and
00:01:25.439 there's a banner up at the top if you want to do it on your laptop that goes right to the slide deck.
00:01:43.040 Cool. Uh, yep. Maselotti.com if you missed that
00:01:50.159 earlier. And oh yeah, we are we are hitting this Wi-Fi hard. Um, right here is our link
00:01:57.439 to those those slides as well.
00:02:04.640 Okay, so why hotwire native? The main goal of
00:02:10.560 hotwire native, the main benefit is that we get to reuse our existing web screens
00:02:15.920 across all three platforms. We minimize effort. We we don't need a native iOS developer and a native Android developer
00:02:21.920 and someone to maintain the app store and Google Play. We can do it all on our own framework of one. We can deploy our
00:02:28.800 own apps by reusing all the business logic we've already built on the rail side.
00:02:35.280 This helps us reduce time and cost by keeping our business logic in Ruby where we want to write code. Uh I look at
00:02:41.280 hotwire native kind of as like the coffee script if it was good. You know, we don't want to write JavaScript back
00:02:46.879 then. And so we wrote c we built coffees script. We don't want to write swift and cotlin. So we have hotwire native to help us minimize that amount of code.
00:02:55.440 It enables faster updates without app store review. Hotwire native is just a
00:03:00.640 little web view that hits our web our rails server on every page load. It's essentially a glorified version of
00:03:06.879 Safari or Chrome with some native well Chrome lowercase C around it. So if we
00:03:12.239 make a change to our rail server, our iOS and Android apps get those updates on the next page load. We can add new
00:03:18.959 features without going through app store review, without having to deal with App Store Connect or deploying a new version. As long as the code exists on
00:03:25.040 the website, we can just get those features for free at the same time across all three
00:03:31.599 platforms. When we're better than that, when we
00:03:37.040 need something more highfidelity, we can unlock native features with progressive enhancement. We can convert entire
00:03:43.360 screens to native maps or native contact lists. We can convert partials to uh
00:03:49.440 bridge components and add native buttons or native dialogues. We'll talk about bridge components a little bit later. Uh
00:03:56.000 I have a lot of content for two hours, so we'll see how much we get through, but we will definitely walk away with both an iOS and an Android app.
00:04:02.879 Uh this is used hotware native is used for base camp and hey they both serve millions of users. Uh DHH said something
00:04:10.159 about millions of dollars of revenue. Um you know these are legit. These are version 10's or version one twos but
00:04:17.840 they serve highowered huge iOS and Android apps. These are not toys. These
00:04:24.400 are made for production and are being used in production. So who am I? U I'm Joe. Hi. Nice to meet
00:04:31.120 everyone. Uh, I'm the hotwire native guy. I've been doing this thing since 2016 when it was still called Turbo
00:04:36.960 Links Native thanks to the person sitting right here, Mr. Eric Stevens. Uh, was my boss and talked to DHH about
00:04:44.560 getting me early access to it. And then we built iOS and Android apps for our app, Beer Menus, at the time in a few
00:04:51.680 months as uh, as the only two developers with a couple hundred controllers, couple hundred screens.
00:04:58.160 I'm the maintainer of the hotwire native libraries along with Jay and the rest of the team at 37 signals. I've launched a
00:05:03.680 dozen of these apps to the app stores and I've advised for twice as many businesses. Um I'm also the author of
00:05:10.479 Hotwire Native for Rails developers. Come swing by the meet the authors event later if you want to chat about that and
00:05:15.919 get an exclusive conference discount code to my book which comes out of course you know next month. Okay.
00:05:22.639 Agenda. Uh we're going to build an iOS app. We're going to build an Android app. We're going to add native tabs to probably only iOS and then we're going
00:05:28.880 to build a bridge component. Anytime you have questions, just raise your hand. I'll answer them. This is an interactive
00:05:34.560 workshop. You're going to be I'm going to be live coding. You're going to be coding. I'm going to give you stuff to do. Uh if you're bored, hot, tired, just
00:05:42.080 leave. Don't don't don't worry. Like totally fine. I I get it. I'm already sweating. Uh do not feel guilty if you
00:05:47.680 need to go or go to the bathroom or take a break. We will take a break halfway throughish um for bio break, but if you
00:05:53.840 need to leave for other reasons, don't I'm not I I won't be offended. Seriously, just get up and go.
00:05:58.880 Uh well, that's for after. Okay, let's do it. So,
00:06:05.520 everyone open Xcode. If you don't have it, there is a hard drive going around.
00:06:11.199 Try don't download it because it will just be really bad for everyone. Um raise your hand if you have the hard drive.
00:06:18.479 Did it already get stolen? Okay, it's over there. Um, if you need that hard hard drive, go grab it. So, open up
00:06:24.400 Xcode. You might see this screen. You might see something different. If you see something different, up in the top
00:06:29.520 left, file, new project or command shift N. If you don't have a
00:06:36.240 Mac, uh, just watch until we get to Android Studio. Mac, this is not XO doesn't run on, um, Linux or, uh, the W
00:06:44.000 word, so we won't be able to work with that. Sorry. When we get here, we're going to want to
00:06:50.160 click iOS, not multiplatform, up in the top, and then click app, and then click
00:06:55.360 next here. This will create an iOS app template. And then we're going to give it a product name. We're going to call
00:07:00.960 this thing blog. And make sure team is just selected to none. Organization
00:07:06.400 identifier doesn't matter. I usually use reverse domain notation. This is how App Store identifies our app in the App
00:07:12.960 Store. So um I'm using com.mmaslotti for my masalotti.com domain. This is the
00:07:18.479 important stuff. You want to select storyboard not swift UI for interface.
00:07:24.080 Definitely language as swift and then testing system and storage can both be none. We can add swift UI features to
00:07:31.520 our hotwire native app but they are features. They're not the framework. So we're going to build our app in UI kit
00:07:37.599 and then we will we could add swift UI stuff in the future. We're not just cutting that off. If none of that made
00:07:43.360 sense, that's fine. Just know that UI kit storyboard is what we want to work with today. Click next. I'm just going
00:07:50.560 to toss this thing on my desktop
00:07:55.680 and click create. So, the first thing that you'll see here
00:08:01.039 is the project explorer. Um, it's a little tiny, so we're just going
00:08:06.960 to see if I can crank this up a little bit. Uh,
00:08:13.440 no, I can't. Oh, well.
00:08:20.000 What we're going to do first is open up something called.
00:08:26.000 Okay. So, up at the top, you should see blog as the app. And then maybe if your iPhone is connected, you see your
00:08:31.199 iPhone. The first thing we want to do is just select that and scroll down to uh iPhone 16 or something. Um, anything
00:08:38.640 that isn't a physical device. If you're on the Wi-Fi network, your device might show up. We want to select one of those things, which is a simulator. Uh, and
00:08:45.920 then click this arrow right here to run the app.
00:08:54.080 There we go. This is this is the iPhone part. And then here is run.
00:08:59.760 We're launching. We're attaching. And we have a simulator. Hooray. Uh,
00:09:07.200 it's a white screen. Doesn't do anything. This is what Apple gives you to get started. Good luck. Um, but it at
00:09:13.600 least gives us a framework or building blocks that we can add to
00:09:18.720 open up scene delegate. You can either double click it or click it once to have it appear. And then I'm going to just
00:09:24.160 hide this left bar over here with command zero. You don't have to do that if you don't want to.
00:09:29.360 Scene delegate is what gets launched, what gets called when the app first launches. This scene delegate function
00:09:36.399 right here. If we read this in Swift, this would be scene. We'll connect to options. The blue uh this gets called
00:09:43.519 when the app is uh launched like we just did. It's where we'll do all of our setup and uh kick off our Hotwire native
00:09:50.240 integration. But Xcode doesn't come with with Hotwire Native installed as awesome as that would be. So we need to add that
00:09:56.080 as a dependency. So on in Rails and Ruby we have gems in on iOS we have swift
00:10:02.720 packages. On Rails, we have Bundler. On iOS, we have the Swift package manager.
00:10:08.160 So, go ahead and click file and then add package dependencies.
00:10:16.000 That third option down. And here we're going to get a
00:10:21.279 essentially a guey that edits a Swift file. Uh, pretty similar to a gem file.
00:10:26.959 And we're gonna type up here in the top right. We're going to go to github.com/hotwired
00:10:32.320 with a d hotwire-native-oss.
00:10:38.560 This is going to give us this URL here.
00:10:50.240 Yeah. Um, hotwire native iOS
00:11:00.000 Up from there, we're going to just select this drop down and do up to next minor version. We are still we are at a
00:11:05.600 1.0, but we are introducing breaking changes at every minor release. So to make sure that you don't get caught by
00:11:11.200 something there for a patch upgrade, we want to select up to next minor. This would be like doing um less than equals
00:11:18.480 and then a major for a gem file.
00:11:23.920 Go ahead and click add package. And we will all download this at the same time.
00:11:29.519 Forgot about this. Uh, okay. Not too bad. When this finishes popping up, this should already say add to target blog.
00:11:36.240 If it says none, just drop down select add to blog and then click add package.
00:11:45.279 What that did was download our entire Hotwire native codebase. Uh, you'll see over here we have package dependencies.
00:11:52.160 There's our code. We got hot wire. We got a config. All this stuff is right here. This is kind of like if we did
00:11:58.000 bundle open. When we try to edit this, do you hear my computer yelling at me?
00:12:03.360 We can't edit this. We uh it's not actually it's a readonly file. So if you wanted to do something there, you'd have
00:12:09.120 to actually download the source or run it from, you know, clone the repo. Uh
00:12:14.560 okay, back in scene delegate. Everyone has hotware native downloaded. Raise your hand if you need a couple more
00:12:20.399 seconds. Cool. Uh what we're going to do next is just
00:12:25.440 clean up our scene delegate here and create our first integration with Hotwire Native. But we can while we
00:12:32.320 wait, we can explore the Hotwire native package. And you'll notice that there's this pack uh directory called demo. If
00:12:38.639 you were to open this re this um directory folder in Xcode, you there's
00:12:44.800 an Xcode project that runs the Hotwire native demo. This is a really good place to start if you want to see kind of all
00:12:50.079 the stuff that Hotwire Native can do. It has native tabs. It has electric bridge components. It has a native view controller. It has path configuration. A
00:12:57.839 lot of things that the documentation doesn't really talk about. Those are really good examples to follow when you
00:13:03.279 want to dive in and explore on your own. It's right here under the demo directory.
00:13:10.880 Lots of stuff going on there. So definitely re recommend checking that out um either after this or or on your
00:13:16.320 own time back at the room. Okay, so everyone now let's pop open
00:13:21.440 scene delegate. I'm going to hide this over here and we're going to delete a bunch of stuff. We're going to delete these comments. Delete this extra line.
00:13:27.920 Delete these comments and we're going to delete this entire thing. We should be left with only the window
00:13:35.360 private variable. uh the scene will connect to function and the guard let
00:13:41.440 underscore obscure obscurity code.
00:13:46.880 Once we have done that, we're going to want to import hotwire native. So go up
00:13:52.240 at the top of your file. We're going to do import hotwire native. This is kind of like a require statement in Ruby, but
00:14:00.480 it's really more like an import in JavaScript. It imports the whole, you know, module, so to speak. It now gives
00:14:06.240 us access to everything under the Hotwire native heavy air quotes namespace because Swift doesn't have
00:14:11.279 namespaces but it does it is does have a package. We now have access to Hotwire
00:14:16.959 native. So inside of the class scene delegate we're want to create a private variable. So
00:14:23.519 we're going to do private let. Let let is a constant. Uh var is a variable like
00:14:29.120 you can see two lines above on window. And we're going to create a navigator.
00:14:34.560 A navigator is the building block of Hotwire native apps. It's what we tell, hey, visit this page. Um, access this
00:14:40.399 resource, go backwards. And navigator provides its own window or view controller for us to present onto the
00:14:46.480 screen. So, we're going to do that. I'm going create a navigator. And this has a couple of arguments here.
00:14:52.399 Oops. Or one argument that we care about this configuration. So, navigator and
00:14:57.760 then give it a configuration. Most of uh Swift arguments are keyword
00:15:06.959 like you would do keyword args in Ruby. You can get rid of them, but they the order always matters and you'll get
00:15:13.040 yelled at if you do it in the wrong order. But for now, just assume that everything is a keyword arcs.
00:15:18.560 A navigator requires a configuration. So we can do ait and then give it a name and a start
00:15:26.079 location which takes a URL. A configuration tells a navigator where
00:15:32.480 to start when it kicks off its web view. Remember, this manages the whole screen, your whole stack of view controllers.
00:15:38.480 So, you need a place for it to well, where do you start? Uh, for name, we're just going to call it posts because
00:15:44.320 we're going to be working with a blog demo. I'll get to that in a second. And then for start, we're going to just give
00:15:50.240 it root URL, which, uh, if you might have noticed, doesn't exist yet.
00:15:56.320 up at the top outside of our class scene delegate, we're going to give a constant
00:16:02.880 and call root URL and it's going to equal a URL pointing to localhost 3000.
00:16:12.160 This is a URL that takes in a string.
00:16:17.680 At compile time, Xcode cannot tell if this is a real or valid URL. So, it's
00:16:22.959 going to return something called an optional. An optional is like every variable in in Ruby. Everything is
00:16:29.040 optional. Anything can be nil at any time. In Swift, it must either maybe nil or never be nil. By throwing that
00:16:35.600 exclamation point at the end, we say, "Hey, trust us. This is definitely not nil." Uh, spoiler alert, if it was nil,
00:16:42.639 the whole app would crash. But writing invalid URLs is actually pretty difficult. Um, here we're just saying
00:16:49.519 URL now is a URL value and it will never be null.
00:16:59.759 We're going to take a break from Xcode and open up our browser here.
00:17:09.839 Hold on. So, this is the demo app that I had instructed everyone to download. If
00:17:15.839 Raise your hand if you played around with this at all. Okay, it's a blog. It's boring. Um, you have blog posts,
00:17:22.799 you have edit screens. It's a CRUD app for three blog posts that should be seated. Uh, does everyone have this on
00:17:30.640 their machine and has it running? Raise your hand if you do not. Okay. If
00:17:37.760 you do not have this running, we're going to want to clone uh this repo here, Joe Maselotti Hotwire Native Blog
00:17:45.679 Demo with dashes. That is extremely small.
00:17:50.960 Thank you for the squints in the audience there.
00:17:58.720 So, we can clone that to our local machine and then run bin slash setup which will get everything running. Um,
00:18:05.760 it's a pretty bare bones Rails app. There's nothing special in it except we
00:18:11.200 just want to have the database seated which will get run at the end of bin setup and then kick off your server.
00:18:24.000 go to the top.
00:18:30.160 So once you have that running on your machine and you can access localhost 3000 in your browser,
00:18:37.760 we can then tell our our iOS app to actually visit that first page. So after
00:18:44.799 our guard let statement, we're going to want to grab a reference to our window and assign the root view controller to
00:18:51.039 the navigator's root view controller. What this is doing is it's saying,
00:18:57.840 hey, this window up here, this is the UI window. This is what the
00:19:02.960 user is seeing, the entire UI of our app. We're going to set the root view controller to the root view controller
00:19:10.240 supplied by the navigator. A view controller is a screen from more or less an iOS. So every screen will have one or
00:19:17.039 more uh view controllers. The root view controller is the one that is the base
00:19:22.160 the core. It's kind of like XML. You can only have one root node. Even when you have tabs, your root node, your root
00:19:28.240 view controller is the tab bar controller. Then it has subview controllers. So here we're saying, hey, let the navigator take over that
00:19:34.559 entirely. And then from there we're going to tell the navigator to start.
00:19:41.440 This will visit the root the start location of the navigator. So pointing to root URL pointing to our local host
00:19:50.000 it's going to hit our rails app and whatever you know root colon there which will be our posts index page. We're
00:19:56.320 going to modify that slightly and just append
00:20:01.520 a path of posts.
00:20:08.559 When we get to bridge components later, this will become more relevant. But when we visit root URL on our rail server,
00:20:16.400 there's no trailing slash. There's no uh path, right? But it renders the posts
00:20:22.240 controller. When our iOS app tries to visit the post controller, it doesn't know the difference between that and the
00:20:27.679 root page. So, we're being very explicit and saying, "Here's the page you want to visit. Don't let Rails do the whole root
00:20:34.240 colon dance in your routes file. Just go right to that page." I recommend this for um both platforms.
00:20:43.120 Okay. From there, we're going to hit the run button
00:20:48.880 or command R. and our iOS app will get a spinner
00:20:56.720 and we have visited our website. Um I've already set up uh if you've run the
00:21:03.039 seeds you will have an account user example password uh it's already populated. So just click sign in
00:21:09.760 and we are signed in with our first hotwire native app on iOS with 20 lines
00:21:15.440 of code. So super powerful. Uh we have a fully working app. We have uh a back
00:21:22.960 button back to the sign-in page. We have a flash message. We have our webbased
00:21:28.080 dropdown. We have buttons. And of course, we can click on links and get native navigation between screens.
00:21:37.120 There's some things that look a little bit weird. Sure, I don't know why we have this navigation bar and that navigation bar. And you know, this is
00:21:43.360 still here. And there's a back button to the signin page. You know, that's kind of odd, but for the most part, we have a
00:21:50.080 core version of our app with very few lines of code, just building off of a
00:21:55.200 very barebones Rails app. Uh, a little bit of diving into how this
00:22:01.360 works. This is a web view. From here down is the web view, right? From here
00:22:08.480 up is native components, native part of uh of coming from Hotwire Native, coming
00:22:14.400 from UI kit. When I click on one of these links, I'm going to turn on slow animations here.
00:22:21.679 When I click on one of these links, we still see that screen behind it and we get a new screen on top of it. That
00:22:28.080 works by when a new link is clicked, Hotwire Native under the hood takes a snapshot of the web view. It then swaps
00:22:36.480 out the web view that's rendering the content, slides the web view around, puts it on the new screen, and renders
00:22:43.120 the the screenshot for you. So we get beautiful native like transitions
00:22:48.640 entirely for free with no changes to our rail server all because of some fancy moving around of a web view and some
00:22:54.880 snapshotting. What's really important about that too is that we're using one web view. So
00:23:00.400 TurboJS means that we can save all of our head requests, all of our CSS, our JavaScript. We're actually doing
00:23:06.640 Turbo.js visits here. We're not doing full page loads. So we get all the page speed benefits of using Turbo.js JS
00:23:13.679 again for free. Um, this is a I mean we're using local host, right? So, we're not even seeing the the spinner or
00:23:19.760 whatever, but if we can keep our our page loads under 200 milliseconds, the user is also going to see no spinner
00:23:26.720 or anything, and it's going to feel very native. And of course, we get things like pull to refresh uh for free. We get
00:23:34.640 if I go to edit this page, these are getting annoying now. Um, we can now edit this screen tile to say, you know,
00:23:42.480 live at Rails Comp. Scroll down to the save button.
00:23:47.760 And you notice that we actually went backwards a screen automatically. Under the hood, Hotwire Native has all of this
00:23:53.760 functionality around when you're visiting a new page where we should navigate to. It knew that this screen,
00:23:59.440 the show page, was on the stack one stack prior. So, it didn't push it again to have duplicate content. It actually
00:24:05.600 navigated us back to that page by default. And all of that was handled in
00:24:11.120 our Rails server when we hit edit or sorry update
00:24:17.200 was redirecting to the post page. So this is how Rails and Hotwire Native plays so nicely together. A redirect is
00:24:24.720 then getting translated in Hotwire Native under the hood to say, "Hey, you want to visit this page? You already have it on the stack. Let's just go
00:24:30.720 backwards." We also made a post request which means that when we go back to our posts index,
00:24:37.679 we have our content without having to run any refreshes or anything. That worked because that
00:24:44.320 remember that snapshot cache I was talking about that got busted every time we make a non-get request. So every time
00:24:50.880 we make a non-get request, we update, we create, we delete, the entire screenshot cache is is destroyed and this page was
00:24:58.320 requested fresh again, which means our content is guaranteed to be up to date. So again, just a really nice way of how
00:25:05.039 Rails and Hotware Native work together. Any questions on iOS, Xcode, Hotwire
00:25:14.080 Native uh before we move on to doing the same exact thing on Android?
00:25:22.880 Everyone up to date, up to speed? Raise your hand if you need a few more minutes to get through this code. Cool.
00:25:32.640 Uh let's quickly, while our folks are catching up, we're going to spend maybe two more minutes here. Let's talk about
00:25:38.240 this statement. Guard let underscore equals scene question mark. Uh what the hell's going
00:25:44.320 on here? Um, we have guard statements in Swift that say
00:25:50.640 a this statement must return true otherwise we're going to run the else
00:25:56.880 statement. This could to this could be rewritten exactly as
00:26:09.600 as this if else. A guard is an unless statement in Ruby. That's all it is. But
00:26:15.200 it's a fancy one because it keeps our code blocks in line. Right? If we have
00:26:20.720 seven guard statements in a row, it's all still going to be one level indented. We have seven if statements in
00:26:26.080 a row. We have what Swift developers love to call the pyramid of doom. We get if if if if. All of a sudden we're 17
00:26:31.919 indents deep and we have no idea what we're doing. A guard statement keeps us at one or zero indents. Uh keeping our
00:26:39.840 code just looking a little bit cleaner. underscore is uh same as Ruby. It's an
00:26:45.279 underscore. It's a throwaway variable. Uh but we're saying here if we can cast a scene as a Windows scene, assign it
00:26:52.080 here. Otherwise, bail. Um if you're bailing there, your app has big
00:26:57.279 problems. I think that that's more of a legacy thing, but essentially it's saying cast this scene so we can start
00:27:03.520 uh applying content to it. We actually don't need that statement at all, but Apple still recommends it
00:27:09.039 because you can run multiple versions of your app kind of like on an iPad like left and right like split screen. And
00:27:15.919 sometimes you your scene will be a a different scene subass. I don't really deal with those in hotwire native apps
00:27:21.600 because if you have two web views running at the same time, you have a lot more sync problems to worry about.
00:27:29.279 Okay. Uh that was iOS. Let's move on to Android. Fair word of warning, Android
00:27:34.960 is like a lot more code and a lot more configuration, a lot more fiddly bits,
00:27:40.559 but we'll come back to iOS for the rest of the talk. So, let's open up Android Studio. Uh,
00:27:47.679 spinner already. Um, you'll either see this beautiful welcome to Android Studio
00:27:53.279 with the new project button. Feel free to click that. If not, file, new, new
00:27:59.760 project. same as iOS or same as Xcode.
00:28:04.960 Don't click this. Don't click empty activity. Click empty views activity. The one without the fancy
00:28:12.880 cube thing in the middle. Um empty activity is going to use jetack compose to create our app. The equivalent to
00:28:18.880 Swift UI not compatible with Hotwire native at this time. Empty views activity is quote the legacy way of
00:28:25.440 doing things. uh views more like UI kit will give you access to the building
00:28:30.480 blocks and the fragment navigation that we need to do an Android app. Click next. Um
00:28:37.679 this usually is pretty good. You can leave this. Just make sure that your save location you know uh where it
00:28:42.720 exists and then click finish. Uh everyone click
00:28:47.919 finish because it's going to download a bunch of stuff. I'm going to talk about this screen a little bit more while everyone else downloads all the
00:28:53.200 dependencies. So click finish. Let Xcode run um
00:28:58.559 language Cotlin. Does anyone know what's underneath that drop down?
00:29:03.760 Yep. Java. Don't click that. Definitely don't click that. Um I got lucky and
00:29:09.120 started doing uh hotware native development when Cotlin was out. So I never had to touch Java, but I've done
00:29:16.000 some old Android apps with Java and it is not fun. Minimum SDK. Uh, Android
00:29:22.080 Studio does a good job of just like selecting this for you automatically. I usually just leave it. Uh, I think that
00:29:28.000 Hotwire Native requires 26 or 28.
00:29:33.760 I'd have to double check that, but um, 28 is definitely good enough for us. And we're running on 93% of devices, which
00:29:39.520 is great. Uh, remember this is across the world. So, if you have develop if
00:29:44.559 you have users that are more likely to have Android devices, think about this number a little bit more. But if you're
00:29:49.679 running an app that is primarily in the US, this number is is way higher. And then Cotlin DSL versus Groovy DSL. Um,
00:29:59.200 hey, look, it recommends it for us. So, we're definitely going to use that one. It's just the way that a file is formatted.
00:30:05.360 Okay, I'm going to click finish. I'm going to maximize my screen. Android
00:30:10.640 Studio is going to do a bunch of stuff down here at the bottom. I'm sure no one in the back can see it. So right here
00:30:17.200 we're importing the Gradel project. Gradel is like bundler on steroids. It
00:30:23.120 doesn't not only does dependencies, but it also sets up our build configurations, our run configurations.
00:30:29.200 It it adds plugins to our actual Android Studio to help us deal with uh parsing
00:30:36.720 XML or parsing JSON and all this stuff. It is a beast and I understand about 25%
00:30:42.960 of it, but we will use it for dependency management. Once your app has finished,
00:30:53.760 you'll want to select the Android from the drop down in the top left in the file explorer. Android is a way of
00:31:00.320 organizing your files that makes sense on Android. If you selected project, uh you will see the file system of what
00:31:08.480 you see under the hood. But if you do Android, you will have a much just cleaner
00:31:15.200 approach to where the files are. So com.maselotti.mmylication,
00:31:20.240 sorry this is tiny. com.mmasalotti my application is
00:31:26.480 actually com folder masalotti folder my application folder app folder and like
00:31:32.159 you know we don't want to see all that in Android Studio so it's smart enough to collapse those into a package like this
00:31:40.240 first thing we're going to do is open app or sorry open gradal scripts and open the second gradal script the module
00:31:47.760 app one there are two gradal script this is for the project and this is for our app we only have one module our app. If
00:31:55.679 you have a lot of code, you're in the right file. If you have a little bit of code, you're in the wrong file.
00:32:02.240 Scroll to the bottom. Here are our dependencies. Here is our Swift package manager, our gem file. Uh, create a new
00:32:08.559 line implementation and then quotes. And in here, we're
00:32:13.600 going to do dev.hotwire uh core
00:32:19.360 version 1.2.3 2.3 and dev hotwire navigation fragments
00:32:26.480 123 1.2.3. Uh this if you're wondering how I memorized those so well, it's
00:32:32.480 because I've typed it a thousand times. But for real, it is on native.hotwire.dev and then clicking Android. You'll see
00:32:39.360 that there is a getting started guide. We're going to be working through this with some caveats for running local
00:32:45.360 development and making it a little bit easier, but if you get lost, you can follow along on native.hotwired.dev
00:32:51.919 and clicking Android. You can also copy paste code, which we're going to be doing in a second because I don't like typing XML live. Uh, but for now, type
00:33:00.799 in these two dependencies and then click this sync now button in the top in the
00:33:05.840 top. This is going to be the same thing as what we did on on iOS in Xcode and is
00:33:12.399 going to download our two dependencies. Why do we have two
00:33:19.039 core and navigation fragments? Well, the Android developers were a little bit smarter than iOS developers when they
00:33:24.880 built the hotware native library and they separated the core presentation logic from the actual vis visual UI
00:33:33.200 layout logic. So in the future we can upgrade navigation fragments to
00:33:38.799 um what was it called? Jetack compose and only have to change one dependency
00:33:44.159 versus on iOS when we want to go to Swift UI we have to actually rewrite almost the entire library. Um I can say
00:33:50.080 all that because I was part of it and I made the mistake so it was me. Uh I'm the problem. Uh but for now we only need
00:33:56.720 both those. Once your two dependencies are added, we're going to do commandshift O to open something and
00:34:03.120 type in manifest. This probably works like you're familiar with a text editor. Uh, quickly open on
00:34:09.679 Xcode and then click enter to open the Android manifest. We need to tell the app that we have
00:34:17.040 access to certain permissions to actually access the internet. So, we're going to add a new line here.
00:34:23.599 open up an angle brace and type in type uses permission. A tab bar is really helpful there. And then we're going to
00:34:29.679 do the internet permission and close that. You'll notice in Xcode or sorry in
00:34:34.800 Android Studio, we let the IDE do a lot of the work for us. Um I don't know if Xcode is just a worse IDE or if it just
00:34:41.359 doesn't have the same mentality, but the Android Studio ID built by Jet Brains is
00:34:47.040 very good at guessing what you want to type next. And that's good because it's way more verbose in my opinion. So, this
00:34:53.200 gives us access access to the internet. We can now access um external resources
00:34:58.800 inside of the application node down at the bottom or at the top. It doesn't really matter. We're going to do Android
00:35:04.880 uh allows clear text traffic and set that to true uses clear text traffic.
00:35:11.920 What this does is lets us access nonSSL endpoints.
00:35:17.359 We're going to be accessing our local host here, which I already asked you to download like seven things. I'm not going to have you set up an SSL
00:35:23.440 certificate. So, we're going to be accessing it through HTTP. This allows the Android app to do it. If you ever
00:35:29.200 have an Android app that's failing for no reason and you're getting zero error messages, it's probably because of this.
00:35:34.240 Uh, you know, know from experience. Up at the top, you now have four files
00:35:40.960 open. If everything went correctly, you have activity main, main activity, the build file, and the manifest. So, we're
00:35:46.240 going to hop over to activity main or command shift O and type in main.xml. XML and hit enter.
00:35:55.280 Here we have the layout of the launch screen or sorry the layout of the application. If we were to run this app
00:36:01.280 by clicking the little green arrow,
00:36:07.119 our Android emulator will launch. Um, that's not the app. That is an app I was
00:36:13.680 working on this morning to make sure I remembered everything. We should see hello world. This hello world is coming
00:36:18.800 from this tiny little hello world right here from a text view. Um, we're not going to
00:36:25.359 deal with this visual layout because we're going to copy and paste. So, we're going to click this little tiny button
00:36:30.800 up at the top right here and get our code version of the XML
00:36:37.520 file. When the app launches, it's displaying this screen. It's the fragment of the main activity and this
00:36:45.280 is what is displaying uh that hello world text. You can see it down right here.
00:36:53.680 So when you have that file open in text editing mode, we're going to go to native.hotwire.dev,
00:37:00.160 click the Android button, and copy and paste this activity main XML file.
00:37:07.920 I could type it, you could type it, but we will both make mistakes and it's just not fun. So, we're going to copy and
00:37:13.520 paste this thing and then I'll explain it.
00:37:29.440 This is giving us one root node, a fragment container view. It's a view
00:37:34.720 that contains a fragment. Uh, a fragment is similar to a UI view controller on iOS. It's essentially a screen or a part
00:37:41.680 of a screen. This one is saying host a navigator host part of the hotwire
00:37:49.599 navigation package. Remember how we use navigator on iOS and we grabbed root view controller from it? That's what
00:37:55.920 this is essentially root view controller on Android. Uh, we're going to make the layout height and width match the
00:38:00.960 parent. We're going to fill the screen and we're going to give it an ID of main nav host. This is how we'll identify it
00:38:07.040 later and how we will be able to access it to push screens onto the stack.
00:38:13.599 Everyone have that copy pasted. Raise your hand if you need a couple of seconds. Cool.
00:38:19.280 Um, this XML XMLNS Android gives us the schema for the
00:38:25.280 Android preface. If we commented or we if we deleted this line of code,
00:38:33.040 we get all sorts of errors because it's like, oh my god, what is this Android colon thing? I've never seen that before. Uh we don't have the name space
00:38:39.200 imported. So if you're again how Android Studio is actually really helpful, uh
00:38:44.720 option enter just does it for us, which is pretty cool. Uh, command shift or sorry,
00:38:51.520 command option L will format and sort a file as well on Android Studio. Command
00:38:58.480 option L. Oh, look. Uh, reformat dialogue. Yeah, that's when
00:39:04.800 it's like, hey, we got rid of a you empty space. Do you want to bring it back? Um,
00:39:10.160 okay. Pop open main activity. I bet everyone knows how to open a new file
00:39:15.200 right now. Main activity.cotlin.kt. We have some errors that we need to deal
00:39:20.640 with, but we're actually not going to deal with it because we're going to delete the whole thing except line one. So, delete everything except line one.
00:39:28.079 The package, this tells Android Studio and really um
00:39:35.040 Gradel where this file lives and what package it's part of. We have access to everything under the commas my
00:39:42.000 application package without having to import it. If we want to access something from a different package, we'd have to import it. Uh on Android, on
00:39:48.640 iOS, we had like one import statement or two. On Android, we'll have one for almost every external thing, every
00:39:55.760 entity that we need to deal with. Pop back open Safari or your browser of
00:40:00.800 choice and copy and paste everything from this main activity. Except the top
00:40:07.440 line into this file.
00:40:13.359 You should be left with this two functions both being
00:40:20.720 overwritten. We're going to dive into the code in a second but I did not want you to make make you type all of this
00:40:26.079 because it is just prone for errors and we can do better. So while you copy
00:40:33.040 paste that or if you have already copy pasted that let's walk through this. First of all,
00:40:39.440 I can hide the import statements with that little carrot or half chevron off to the left, which is nice. We have a
00:40:45.440 class main activity that inherits or implements hotwire activity. Hot activities are
00:40:54.000 hard to explain. Uh activity, we have fragments on Android. They're screens. Activities are almost their own app, but
00:40:59.760 apps can have multiple activities. So normally with Hotwire native you'd have a single activity application. Um maybe
00:41:07.200 you want to do login in its own activity but think of them as entire screens that
00:41:12.400 take over. Uh I rarely will ever use a second activity if ever with hotwire
00:41:17.440 native. So just think about it as your app. Open close parenthesis mean create the
00:41:22.480 hotwire activity when we create a main activity. We could also pass in parameters there if it took any. When
00:41:28.560 this is created, we're going to enable edgeto edge and apply these default time window insets.
00:41:35.119 Sorry, default IME window insets. This is saying fill the screen. How Android
00:41:40.800 13, which came out not too long ago, added a way to extend everything past the screen so you get like nice scroll
00:41:47.040 effects. We're saying no, no thanks. We're going to just we're going to do it our on our own way. Uh which means that
00:41:52.160 we get access to our our action bar at the top a little bit nicer.
00:41:57.280 We're going to set the content view to that R layout activity main. Where's that coming from?
00:42:04.400 When we did Android ID here. Oops. Um, sorry. Layout activity main is the name
00:42:11.359 of this file. It gets it from the the layout directory. R is our resources. Anything
00:42:18.160 that's not code, sometimes also code, can be accessed through R. It's compile
00:42:24.000 time safe, which is pretty cool. we get autocomplete on it and it is checked by grad
00:42:31.680 like hey this file doesn't exist activity main I just heard someone's Android
00:42:37.599 emulator launch that was a good sound um and then we're going to find the view and apply uh navigator configurations
00:42:45.440 this is a list of these configurations hey this looks pretty similar right if you squint this is what we created on on
00:42:51.440 iOS except it was a single entity here we have a list of them because to do with tabs we always want to have a list
00:42:57.760 a little bit of a difference of the API something we're thinking about consolidating but for now know that we're grabbing main and giving it a
00:43:04.400 start location and then referencing main nav host
00:43:12.000 like that it's saying hey put all the content in this screen
00:43:17.359 we'll make one more change to this file we don't want to access the hot wire native demo for this so we're going to change this to
00:43:24.880 localhost 3000. Uh, except we're not because Android doesn't have access to local host. We're going to change it to
00:43:30.880 1002.2. Why? Um, iOS when you access local host
00:43:38.480 creates a roundtrip back to your map Mac automatically. It creates that loop back. On Android Studio, local host
00:43:45.200 actually refers to well local host on the Android device. 10022 is the special loop back to our max IP address. So
00:43:55.680 if you're ever getting an error of like, hey, I don't have access to that. What the hell's going on? Um, it's probably you're accessing local host on Android,
00:44:01.520 which is never should never be a thing. Hit run or rerun. And if all went well,
00:44:09.200 we should all have uh an Android app running on our emulator.
00:44:16.880 And we do. Looks a lot like our iOS app, which is good. We can sign in. We have
00:44:21.920 our flash messages. We have our, you know, content here. We have our drop downs. Uh, a little bit more code, sure,
00:44:29.440 but we have technically feature par across both
00:44:34.480 platforms with just a little bit of code there.
00:44:41.119 Okay. So, that is kind of the first half of the workshop. That's the basics. What
00:44:47.440 we're going to do right now is I'm gonna take a few minutes. If you want to do a quick restroom break, we still have 75
00:44:54.319 minutes left. Now is a good time to quick take a quick bio break. Uh it's 11:03. At 11:10, I'm going to get
00:45:01.200 started on the next part. During that time, I'm going to come around and help anyone that needs help. We have 7
00:45:06.480 minutes, so go fill up your water if you need it or just hang out. I'm going to come around and raise your hand if you
00:45:12.000 need help. Okay, so for everyone who spent that entire time downloading Gradel, we're
00:45:17.040 going to be done with Android Studio and go back to iOS. Sorry. Um,
00:45:23.440 yeah, I couldn't think of a good way to make sure everyone had all of the Gradle dependencies without asking them to do
00:45:29.839 the workshop on their own. So, I kind of bet on having faster Wi-Fi. So, I
00:45:35.440 apologize if you didn't get a chance to do the Android stuff. I will be sticking
00:45:40.640 around and you can definitely come chat with me or we can do a little pair programming session. Um, just grab me
00:45:46.880 after this talk. I'm going to stay in this room until they kick me out.
00:45:52.079 Okay, so we have an app. Uh, we can go three directions now. We can make the
00:46:00.160 app look a bit better on iOS, make it feel a little bit more native. That's
00:46:05.200 option one. We can add native tabs. That's option two. Or we can do a native
00:46:11.520 bridge component which would be adding a button into the top right of the screen. Uh does anyone have a strong preference
00:46:17.920 for either of those? Raise raise up a finger. One, two, or three there.
00:46:25.599 One was tabs, two was or one was uh native screens, two was tabs, and three
00:46:31.119 was native component. A beautiful distribution.
00:46:40.000 Awesome. Okay, we're going to try to do them all. Um, tabs is first or sorry, making this making this a little bit
00:46:45.599 look a little bit nicer is first. Okay, so we're going to do this. We're going to put this over here.
00:46:51.280 We're going to put our brow or uh I have Vim over here and we're going to go to
00:46:58.160 the posts show page. This is the code that I downloaded from
00:47:04.240 the git repo and what's rendering is the content right here. So the first thing
00:47:10.240 that I can do is just pretty cool and say again hello there
00:47:16.960 add a p tag and then pull to refresh this and we get our content. Now, that
00:47:24.079 might not seem that magical, right? Like, like, cool. We added some text to the screen. But you'll notice that we
00:47:30.079 didn't open Xcode. We didn't submit a new app to the app store. We didn't touch any iOS code. And if I was running
00:47:35.599 the Android app still, it would also have that functionality. So, pretend that hello there was a huge bug fix. We
00:47:41.040 just deployed our server. Uh, we just got bug fixes. We just got new features. We just got whatever we can do on the
00:47:47.280 website without any native integration. If you take away one thing from this
00:47:52.640 workshop, it's that. It's the power that once you build your iOS and Android app,
00:47:58.000 that first version, it can sit in the app store for a really long time, but still get updates every week or every
00:48:04.560 two weeks or every 3 hours if you want because you're still just deploying to your Rail server. I had an app in the
00:48:10.640 app store for so long without updates. Still receiving bi-weekly or fortnightly updates to the rail side that Apple
00:48:17.440 emailed me and said, "Hey, we're going to remove it because you haven't updated in two years." Um, I had to then rebuild
00:48:23.599 the app and repackage it and resubmit it, but didn't make any changes. It was still getting new features, but I didn't change the native code for that long
00:48:29.920 because we didn't need to. Uh, so let's talk about this. Here we
00:48:35.680 have the back button. the native title and then the web view,
00:48:42.000 right? The web view starts right here and we have blog and the hamburger menu. This is all part of the website, right?
00:48:48.319 We can tell by doing pull to refresh and seeing what slides versus what doesn't. Um, this feels really out of place on a
00:48:56.480 native app, especially when we look at this screen and we have like posts and blog and posts and all this. Like
00:49:03.359 there's a lot of just junk up here. So, what we're going to do is hide all this on our native apps just to make them
00:49:08.480 feel a little bit more native. So, to do that, we're going to open up the layout application file and we're going to go
00:49:14.640 down to our stylesheet link tag and create a new one called native, but only
00:49:20.000 import it if a hotwire native app.
00:49:26.240 This is now saying on everyone gets application.css, CSS. But if the user
00:49:31.839 agent identifies itself as a hotwire native app by having the string hotwire native or turbo native in it, uh we will
00:49:38.160 then also want to include the native CSS file.
00:49:43.520 We'll create that in app assets stylesheets native.css.
00:49:52.960 And we'll say I'm using Bootstrap, so we're going to use Bootstrap. uh D hotwire native
00:50:01.920 hidden and give it a display hidden and of course my favorite important
00:50:10.000 we can do this also with Tailwind CSS with a custom variant. I have a blog post on that about how to do a custom variant which would give you access to
00:50:16.400 things like hotwire native you know hidden classes and not hotwire native
00:50:23.520 hidden classes. But for this for this demo, we can just do that.
00:50:30.160 Up at the top, we can open up the shared header file. This is app views shared
00:50:36.800 header partial. And this is where we're rendering out this
00:50:44.400 um this title right here, this H1, right? Oops. And we can just throw a
00:50:50.720 class on that called d hotwire native hidden. Refresh the
00:50:56.559 page.
00:51:05.599 What did I do wrong? All right, some live debugging. I think
00:51:13.440 I need to
00:51:21.920 Okay, maybe the maybe this was wrong.
00:51:28.240 So, what we're going to do here is just include it for every file. We're going to get rid of that if statement and first check that.
00:51:46.000 Thank you. That is exactly correct. Okay. So, let's go back to what we had.
00:51:52.800 Okay. Thank you. Um, live coding, right? So, we now have display none, the
00:51:57.839 correct CSS class. Uh, it's now hidden from our iOS app, our Android app as well. And if we go to our website,
00:52:06.240 it still shows up. So, this is now just including that CSS file on the native
00:52:12.319 apps. It means that we can do things more exciting than just hiding things. We can add buttons in. We can say, "Hey,
00:52:18.079 this button should only exist for the apps." Um, kind of like these sign out, add a post buttons. Like those probably
00:52:24.000 make more sense in the dropdown. So, we could do the opposite of it and say not hotwire native block, right? And have
00:52:30.559 something display as a block element and hide them in the app. Uh, next up, we're going to hide the whole
00:52:40.640 navigation drop down. So, I'll let you do that if you've
00:52:45.680 already implemented it. There's a file called under the shared directory called navbar. And if you just followed along,
00:52:52.079 you should be um you should be able to add that class and hide that as well.
00:53:11.839 So here's the change here, right? Dehotwire native hidden and we pulled to refresh and we've now hidden it. It is
00:53:19.760 now hidden in our iOS and Android apps. Um, again, no App Store deploy, no
00:53:26.240 changes to native code, nothing that we needed to do for the client side to get what I think looks way more better, way
00:53:34.079 better uh as a native app here without all of that kind of junk going on up at
00:53:41.920 the top that doesn't that makes the app feel less native, right? One word of advice here. We never want to replicate
00:53:49.119 or duplicate native UI elements in CSS. iOS 26 is a perfect example of why. All
00:53:56.480 of a sudden, iOS 26 came out or is coming out and everything looks like frosted glass. Now, all of your changes
00:54:02.720 that you made to make your fancy, you know, Rails rendered iOS app look like iOS 18 need to be redone again for
00:54:09.359 Glass, right? So, what I like to do is make sure that I build the web interface, but make sure that the
00:54:15.119 elements blend in seamlessly with the native elements that already exist. Don't try to recreate element styling.
00:54:21.680 Use native elements where possible and just make it look more native like that
00:54:29.040 make sense. Uh, also good luck trying to keep iOS and Android, you know, design in sync,
00:54:35.839 right? No one wants to do that. Okay, so last thing we're going to talk about here is like we have that nice little
00:54:41.920 posts title that's native, right? This little posts thing that is a native element right there.
00:54:49.200 We go to hide content with Tailwind CSS. We also have that native element.
00:54:54.559 But where is that coming from? So by default iOS and Android will pull the
00:54:59.680 title of the page.
00:55:05.359 this one, the title tag in the head element and put it in the title bar of
00:55:11.760 the screen that you're looking at. So, if you notice, if you're quick, you'll notice that there's no title when
00:55:17.760 we click into it and then it loads after the page loads. See how it flashes in there?
00:55:27.359 That's because it's waiting for the page to finish loading so it can actually grab the title element and then slapping
00:55:32.800 it in there. This is a really big but small improvement that means that we never have to manage that title with a
00:55:38.640 native component or anything. All we have to do is set the title tag in our HTML and we're good to go. So here I'm
00:55:44.960 using a you know fancy content for title or it's defaulting to blog in our shared
00:55:51.440 shared header which is on every page. We then are setting content for and then
00:55:57.920 putting it in that H1 there. So, like, you know, I've kind of abstracted it into something that's that's nice and reusable, but you could have what I
00:56:04.640 usually see in in client apps is content for native title or content for title or
00:56:10.960 a default thing. That way, you can have something that's short and sweet for your native apps and then have it
00:56:16.240 default back to your your website or, you know, marketing whatever on on the web. Again, you can change those without
00:56:23.520 any changes to your iOS or Android apps because it's all just HTML.
00:56:29.359 So, we now have an app that's looking, I think, pretty good. We don't have that status bar. We don't have that nav bar
00:56:35.680 up at the top, but we now have no way
00:56:42.319 to get to comments. We've just removed the comments functionality from the iOS and Android app. Right? If we can't link
00:56:49.359 if you can't link to it or if it's not linked to, it doesn't exist in iOS and Android.
00:56:55.280 by contrary or by you know inverse if you link to it it exists. So here we've essentially said, you know what, we
00:57:00.720 don't you don't need the comments in your iOS and Android app. Sure, but I'll
00:57:05.839 let the product person decide if that's actually how we want to do it. We're going to add those back in with a native
00:57:10.960 tab bar. Okay. Before I move on to native tab bars, any questions around
00:57:17.040 this idea of making the app feel a little bit more native by updating the web interface?
00:57:22.880 Custom CSS. Cool.
00:57:28.319 uh masalotti.com. Um, and then
00:57:34.240 if I go to articles and I searched for Tailwind,
00:57:39.760 I have the variant, the custom um custom variant
00:57:44.799 that you need to do things like hotwire native colon and not hotwire native
00:57:50.559 colon. So you get full full access to all of your uh modifiers
00:57:58.480 in Tailwind and CSS. So changing background colors, changing shadows, changing rounded edges or whatever. Uh
00:58:03.760 it works just like any other variant like SM or MD or LG uh you can do or
00:58:10.079 dark mode. You now have access to that on Tailwind. So really helpful there if you're using Tailwind in your app.
00:58:18.240 Okay, let's do some more Swift. So, we've now decided that we need our tab
00:58:24.960 functionality, right? We've we've removed it from the web, but the iOS and Android apps don't have don't have a way to get to comments, and we don't want to
00:58:30.720 just add a link to the bottom of the screen. So, we're going to add tabs. So, we've now crossed that bridge where we're adding native functionality. We
00:58:37.040 need to edit native code and submit a new app bundle to the app store, wait for approval, you know, the whole nine
00:58:42.319 yards. But we need the native tabs. So, we're going to do it.
00:58:47.839 First thing we're going to do is delete our navigator. We're going to delete well actually
00:58:54.000 we're pretty much going back to square one. Delete our navigator. Keep everything except the root URL or get
00:58:59.359 rid of everything except the root URL and replace this with a private let tabar controller of a hotwire native tab
00:59:08.880 hotwire tab bar controller with uh no parameters there in the initializer.
00:59:22.960 Down under our guard statement, we're going to do the same thing we had before. Window.rootview controller equals the tab bar
00:59:28.720 controller. We don't call tabbar controller viewcontroller or root viewcontroller because it is a view
00:59:34.319 controller. It is an abstraction on top of the native tabar controller. Not an
00:59:40.720 abstraction, a subclass of. So we can just set it directly to the root view controller. And just like we had before, tab barcontroller dot
00:59:48.960 um instead of load we're going to call instead of start we're going to call
00:59:55.359 load. Load takes an array of tabs.
01:00:01.200 Right? So we're going to pass in a variable called tabs which doesn't exist yet.
01:00:09.280 So to create our tabs, we're going to going to go outside of the class scene delegate. This is just to show that we
01:00:16.640 don't really need this to be part of scene delegate. We can be getting this from the network. We could be getting it from a bridge component, but it is
01:00:22.079 external to scene delegate. So we're just going to kind of create it in the local name space with a variable called
01:00:27.760 tabs. But again, you could swap out that for any number of of ways of creating these
01:00:34.799 tabs. We're going to create a hotwire tab
01:00:41.359 which create takes a title an image and a start location which is a root uh
01:00:54.720 lots of errors. Oh, URL.
01:01:05.760 So, it takes three parameters. Uh, a title, which is going to be the title of the tab at the bottom. An image, the
01:01:10.880 image that we want to use to render as the icon, and then a root URL, or sorry, a URL for where it should start. If you
01:01:18.160 squint again, this should look very similar to our navigators configurations that were created earlier. That's
01:01:23.520 because under the hood, this will create a navigator configuration, and we'll just slap the image onto the the visual
01:01:28.880 tab. It'll use title and URL to create the configuration. I'm going to format this a little bit
01:01:35.119 nicer. And we're going to do the same thing as before by doing uh root um posts.
01:01:44.000 And we're going to call the title posts with a capital P because this is what is actually going to be displayed on the
01:01:50.240 tab and as the title of the the root screen.
01:01:55.359 So that would create one tab for us without an image. But we can't do that
01:02:00.640 because nil is not compatible with UI image. We're essentially saying the Xcode is asking us for an image and
01:02:06.880 we're saying no. So Xcode is saying no thanks. Uh we need to supply an image there. So we're going to create a UI
01:02:13.920 image and give it a system name which is a string and then force unwrap that. And
01:02:20.319 then I'm going to crash everyone's app while everyone's going to run oh sorry system
01:02:26.720 name. And then everyone's going to click command R or run. And we're going to watch our app crash.
01:02:35.280 Fatal error. Unexpectedly found nil while unwrapping an optional
01:02:41.599 value. We told Xcode that this image definitely
01:02:47.119 exists. We're sure of it. So we force unwrapped it with an exclamation point to make it non-nil. But there is no
01:02:53.280 system name image with no string. So Xcode said uh Xcode killed our app and
01:02:58.480 we would get a crash report in app store connect or in test flight if we were you know still doing beta testing. Um so and
01:03:05.040 then we get the error down here and the line number and we also get this little debugger thing with our stack trace.
01:03:11.920 Just a little help on there on debugging some stuff. This would be pretty easy to debug especially when we run it in Xcode
01:03:18.880 because we have the line that it crashed on. Uh, one of the nice things about a compiled language, we can actually see
01:03:24.559 this right in our IDE or in our text editor versus like, you know, Rails, getting it in the browser or what have you. So, one of the reasons I do like
01:03:32.640 writing some native code every now and then to fix the issue, we need to supply a system name. Does anyone know where
01:03:39.359 we're going next? SF. So, SFols is an app that I did not
01:03:48.799 tell you to download. I apologize, but we're going to look at it on mine. Uh, you can download it for free from the Xcode developer website. These are 6,44
01:03:59.760 symbols that we can use in our iOS apps for free with no attribution. Pretty freaking awesome. Uh, they're bundled
01:04:07.039 with the OS. We don't have to worry about copying and pasting them into their app like we do on Android. We can
01:04:12.240 just use them. The only thing we have to watch out for is when they have a little eye icon next to it. Those can only be
01:04:19.200 used for specific features. This symbol may not be modified and may only be used to refer to Apple's markup feature. So,
01:04:26.720 as long as there's no little I, you're free to use them. We're going to find something called
01:04:40.559 newspaper. Looks good. So, we're going to use this newspaper icon, right? I'm
01:04:45.839 going to rightclick it. and click copy name and go back here and paste it in to our
01:04:53.520 system name string. Uh, everyone can do this. You don't need SF symbols. SF symbols is just an app to browse these.
01:05:00.640 iOS already has all of these installed has all of them bundled. So if you lowercase newspaper one word into system
01:05:06.640 name there and then click command R to run, we will all see the app show up
01:05:12.000 with our single tab of posts down at the bottom. Can everyone
01:05:18.319 see that in the back? Good. Cool. Uh
01:05:23.760 there you go. Those on the left. Uh so we have our we
01:05:29.280 have one tab which looks super awkward. It's just kind of like floating down there. Uh, so what we're going to do is
01:05:34.880 we're going to just change a little bit of how our app looks to make it feel a little bit better. Open up app delegate.
01:05:40.799 Commandshift O for quickly open. Uh, same thing as Android Studio or most,
01:05:46.079 you know, text editors. And we had a whole bunch of junk. We're going to do
01:05:51.280 the same thing that we did in our scene delegate. We're going to delete everything except the application did
01:05:59.839 finish launching with options function that returns true.
01:06:05.839 We're going to import hotwire native at the top. And actually, we're not going to import. We're going to say UI tabbar
01:06:13.039 appearance equals a net. Oops.
01:06:25.440 UI UI tab bar appearance.
01:06:39.119 Am I spelling it wrong? Thank you.
01:06:58.720 There we go. So, we're going to do UI tab bar appearance.croll edge appearance
01:07:03.920 equals open close parentheses. What this is doing is giving us an opaque tab bar.
01:07:10.799 Right now, we have a translucent one. You can see that the white of the web view just bleeds right into the
01:07:17.280 background of the tab bar. I can further show this by opening up our native.css file and giving it a HTML
01:07:24.880 background of orange. Important.
01:07:30.559 And also giving our body a background of orange. And if I
01:07:35.680 refresh this page, you'll see that the orange bleeds through to the tab bar.
01:07:40.720 Right. Apple decided with iOS 17 or 16 or something that this looks better. I
01:07:46.960 think it looks great until we get to something that is a scroll view and we have the con oops the content bleeding
01:07:53.839 through it. Also that whatever the hell that is. Um, so here we can when we have
01:07:59.440 our scroll edge appearance equals a knit, we're getting an opaque one. And our opaque one will always give us
01:08:07.599 this exact kind of frosted look of um of the background color. When we get rid of
01:08:13.520 the orange, it'll look a lot nicer. We now have this nice Oh my god.
01:08:23.440 Wow. It looks totally different on my on my machine. Um, hopefully everyone can see it on their machine, but this is
01:08:29.440 gray on my machine with a with a a line on it.
01:08:36.400 Oh my god. Um, can I move it around? Let me see if I can make it bigger.
01:08:48.560 No, now I can't scroll it. Cool. Uh, four. Anyway, this makes it look a lot
01:08:54.159 nicer and we get a nice gray background. But most importantly, we want to add a
01:09:00.239 second tab. So, I will let everyone here add a second tab to this list of the
01:09:07.920 tabs array. We're going to be using a root URL of the comments path. And if
01:09:14.880 you has have SF symbols installed, you can look for a system name. If not, I'm going to give you What should we search
01:09:22.239 for? Throw out a name. Speech bubble
01:09:29.120 bubble. Bubble. Feel free to use any of these bubble names. We have bubble.
01:09:34.640 Bubble. Bubble. Circle. Bubble. Bubble. Explanation. Bubble. Um, I usually use
01:09:41.199 bubble.
01:10:17.679 Something really cool about SF symbols is that you can customize them right in SF symbols. So here I'm creating a star
01:10:23.040 bubble with a pink uh border and a red or I guess this is technically pink and
01:10:29.120 this is purple um coloring. And we can do all of this in code as well. by using
01:10:35.520 tinting. So this is the primary color, this is the secondary color, this is the tertiary color. And we can tint this by
01:10:43.040 using a blending mode and get this in code without having to actually like download this image. These are all SVGs,
01:10:49.679 right? You know, you used inline SVG gem. It's essentially the same for hero icons, but on iOS and can give us lots
01:10:57.520 of customization options there. So, I'm going to implement what I just
01:11:03.760 talked about. We're going to create a hotwire tab. We're going to give it a title of
01:11:08.800 comments. We're going to give it an image of UI image system name.
01:11:16.159 I forgot bubble. Thank you. Bubble.
01:11:22.320 Force. Unwrap it. Give it a URL of root URL appending a path of comments
01:11:28.960 and then close out that tab and command R to run our app and we will
01:11:34.960 have two tabs at the bottom of the screen posts and comments. When we click
01:11:41.600 on the comments tab, it will load our comments screen. These have their own navigation stacks of
01:11:48.960 screens. Um, so if I go to posts and I go like, you know, two maybe three levels deep and I switch over to
01:11:54.960 comments and then I go back, we keep those stacks just like how a native iOS or native Android app would work. We can
01:12:01.840 also click the tab again to pop back to the root. So all functionality that you get for free in iOS apps, we also get
01:12:09.679 for free in hotwire native because we're using a tab bar under the hood. We're using a real native navigation bar, a
01:12:17.520 native tab bar controller, something that frameworks like React Native often don't use. They often want to make their
01:12:23.360 own. They often build their own and render their own on the screen. We're using something that is part of iOS.
01:12:31.280 And the big the biggest one of the biggest benefits of this is if we start looking at future versions of iOS. I'm
01:12:38.640 now going to open this exact project in Xcode 26 beta. I'm going to run this on an iPhone
01:12:45.679 running iOS 26. I'm not going to make any code changes. I don't know why Xcode
01:12:51.040 is bouncing at me. Um,
01:12:56.159 we're going to launch this in the iOS 26 simulator.
01:13:06.159 Still building in the back there.
01:13:14.640 First time you launch an app in the simulator, it takes a few minutes. And once this finishes loading,
01:13:31.840 let's try again.
01:13:51.840 Okay, installing.
01:13:58.560 Someone asked why I didn't have everyone download Xcode uh 26 for the demo, and
01:14:03.760 you can see why now.
01:14:23.760 Okay.
01:14:32.800 So without any code changes, we now have our frosted glass tab bar controller at
01:14:38.719 the bottom, no changes to our iOS app, no changes even to our Rail server. If someone was running this on their
01:14:45.760 machine, they would just get all of this with the new Frosted Glass. Looks a lot better on my computer here. I apologize.
01:14:52.159 Uh for free with no changes because we're using native components under the hood. We're not creating this tab bar on
01:14:58.719 the web and have to worry about updating it per platform. We're just using what iOS and what Apple gives us.
01:15:08.320 All right, enough of the beta. Let's go back to our
01:15:16.239 regular app on Xcode
01:15:22.320 and give this a run. That wraps up tabs. Uh, if you wanted to edit a third tab,
01:15:27.360 you know how. If you wanted to add a fourth or a fifth, you know how. And sometimes I will make this variable,
01:15:33.360 like I said, request from the network. So you can have an endpoint on your server that returns this as JSON. And
01:15:38.800 now all of a sudden you have dynamic tabs depending on the user who's signed in, depending on if they're logged in or not. And all that logic is on your
01:15:44.960 server. All iOS has to do is know how to make that network connection and have a fallback. Very powerful. I use that for
01:15:51.440 my client apps very frequently because it means that once I disengage from a client, they don't have to talk to me to
01:15:58.320 update their native code, right? they can just update a JSON endpoint on their server and now all their users has a
01:16:03.840 whole new suite of of tabs or like one tab is broken, they just hide it from the JSON file for a little while.
01:16:09.920 Really, really powerful and just means that I can interact with the client way
01:16:15.520 less before they have me build something new instead of modifying existing things.
01:16:22.560 Any more questions on tabs before we move on?
01:16:28.480 Cool. Okay, so we talked about uh how great
01:16:35.360 Hotwire Native is. We talked about how we can interact with the tab bar controller. We talked about how we can
01:16:40.560 make the screens feel more native. We have about 30 minutes left. Uh we're next we're going to dive into something
01:16:46.880 called bridge components. Raise your hand if you've heard of bridge components.
01:16:52.239 Bridge components are a way to bridge the gap between web and native. They
01:16:58.239 give us access to native APIs without having to write entire screens in native
01:17:04.080 code. So if we go to
01:17:10.159 I'm going to open what's a good way to demo this. Um, the documentation
01:17:18.159 has a little bit on what a bridge component is,
01:17:23.360 but what it's showing is this profile button up in the top of the native
01:17:30.080 Android action bar. What we're going to do is add something similar, but we're going
01:17:36.159 to work with an alert. So by default an alert is a native swift or cotlin
01:17:44.000 feature right or iOS or Android feature. We can't trigger alerts by writing web
01:17:50.320 code right we need a way to bridge the gap so when certain HTML happens or
01:17:56.080 certain JavaScript happens on the web we trigger native code on the client and we
01:18:02.159 do that through these things called bridge components. So, the first thing that we're going to do, it requires a
01:18:07.360 little bit of setup to get going. Our iOS app is already set up to work with Bridge Components out of the box, but
01:18:12.640 our Rails server needs uh a new package, right? So, we're going
01:18:19.280 to open up our directory for where we have our demo app on our machine
01:18:25.920 and we're going to do we're going to pin
01:18:31.520 with import map the hotwire native bridge. This gives us a pretty small
01:18:38.880 library that subclasses stimulus. So, instead of creating stimulus
01:18:44.719 controllers, we're going to use bridge components. Bridge components are subasses of stimulus controllers. So
01:18:51.280 once we pin that, we will have
01:18:59.760 down at the bottom of our import map file, the hotwire native bridge is all
01:19:04.880 set up for us. There's no other configuration we need to worry about. From there, we're going to do bin rails
01:19:12.719 g stimulus bridge slashconfirm.
01:19:18.320 This is very similar to creating a new bridge component with stimul or sorry, a new stimulus controller, but we have that slash in there, which is going to
01:19:25.120 give us a nested directory. So, it's going to put it under controllers/bridgeconfirm
01:19:30.560 controller. I like to keep all of those kind of not kind of, but keep them separate for bridge controllers because
01:19:35.760 they're going to do different things. We're going to open up that confirm
01:19:42.159 controller. And we have a regular old uh
01:19:49.520 stimulus controller here. I always forget the syntax for bridge controllers for bridge components. So we're going to
01:19:54.719 go and copy and paste and then I'll show you what it is.
01:20:00.480 So the change there that I made was we instead of importing a controller from hotwired stimulus, we import bridge
01:20:06.480 component from hotwired hotwire native bridge. And then our root class is no long that we're extending is no longer a
01:20:13.360 controller. It's a bridge component. These work almost exactly like a
01:20:19.440 stimulus controller. Bridge component extends stimulus controller. It just adds a little bit of functionality. All
01:20:25.040 of that functionality is documented on this bridge components oops reference
01:20:32.320 bridge components. All of the different elements and access
01:20:37.600 and properties that we have access to is all documented there. I'm not going to get into anything more than just one. So
01:20:43.120 if you want to check that out, go there. For our stimulus controller to talk to
01:20:48.960 our iOS or Android app, we need to wait for them connect. We connect them through a name. So, we're going to do a
01:20:54.159 static um oh god sorry
01:21:02.640 static component equals confirm. This name of component is what's going
01:21:09.040 to allow us on the iOS app to connect the two as the confirm component. On
01:21:14.480 connect, uh for now we will do console.debug debug connected.
01:21:24.800 So when this connects, we're just going to see some logs in the uh the web browser. Right on our show page, we're
01:21:33.040 going to add a button
01:21:42.400 to nowhere. And it's going to be a data
01:21:48.960 controller bridge confirm.
01:21:55.199 And this means when it connects, we're going to fire that console.debug connected.
01:22:01.520 Yes, thank you. So we're under a namespace of bridge, right? So we have the bridge directory. So those will
01:22:07.600 always be two dashes. If this was confirm fu, it would be confirm one-ash
01:22:12.800 fu. But because we're in the con the bridge directory, we use two dashes in
01:22:18.239 between those. When we go to our website,
01:22:28.960 we go to the show page. We have our confirm, but we don't see
01:22:34.400 them in our logs. We don't see it in our logs because these components only fire
01:22:40.480 for your hot wire native apps that can that actually can implement them. So we're not seeing the connect in the logs
01:22:46.719 because the web server the website doesn't have the bridge component counterpart installed.
01:22:53.760 Opening up Xcode, we're going to create a new file. So, uh, under the blog folder, we're
01:23:00.400 going to do new empty file and call it confirm
01:23:06.719 component. And in there, we're going to import hotwire native
01:23:13.280 class. Confirm component implements a bridge component.
01:23:18.560 static var name
01:23:31.520 uh static bar v bar
01:23:37.199 and so this is like the absolute basic
01:23:44.080 component. It doesn't do anything but it does connect. Uh remember confirm is the name that we had
01:23:51.760 here as the name of the component. The last thing that we need to do to actually get this to wire up is to open
01:23:58.480 up our app delegate and before the app actually launches we need to register
01:24:03.520 this. So hotwire native oops hotwire.registerbridgeidge
01:24:09.520 components and then the confirm component self. Think of this as your
01:24:14.960 stimulus manifest. This is where you tell the under the hood the hotwire
01:24:20.800 native like hey this app right now can deal with the confirm component. We have the ability to work with a something
01:24:28.400 called a confirm component under the hood. That's actually just adding confirm to the data to the uh user agent
01:24:34.960 which we'll look at in a sec. But that's how the website knows and the JavaScript knows that we can handle it.
01:24:42.800 So when we fail subass must provide a unique name.
01:24:57.600 I think I got that backwards.
01:25:07.040 Oh, see that was so close. So right now we have an error that says the bridge component subclass must provide a unique
01:25:13.679 name. I thought I was but it's actually a override class var. This is saying
01:25:22.000 that we want to override the class or static variable name from the bridge
01:25:27.280 component superlass and provide our own. So the only reason that we have that is because of what we just saw the the
01:25:33.280 runtime error. So give this a run.
01:25:38.880 We should get no errors this time. We don't don't need this.
01:25:47.520 I don't want to run us on 26. I want to run us on 185 just to remove any variables there.
01:25:55.280 And when this runs, what we're going to do is we're going to connect the Safari debugger to the iOS emulator to see our
01:26:02.320 console logs. So, this app is going to launch. I'm going to go to the show page and then I'm going to open Safari and click
01:26:08.639 develop. And you'll see that I have the simulator right here. Local host one is
01:26:14.320 the posts one page. We now have access to
01:26:20.080 our HTML web view inside of our Hotwire native app. So we can do all sorts of
01:26:25.280 things like document.body body dot style
01:26:30.400 equals background orange and interact with this through a
01:26:35.520 JavaScript console. Well, we can command R to refresh the page and trigger a full page reload. We have it's just like
01:26:42.159 debugging our regular web app, but it's inside of our Hotwire native app. The
01:26:48.480 same exact thing works on Android through Chrome. If you go to the chrome colinsspect
01:26:55.440 special URL, you'll see the list of all of your Android emulators up there and you get the same interface just in
01:27:01.280 Chrome. So, right off the bat, we can see really Can I make this bigger? Oh, nice. We can
01:27:08.400 see that we have connected. That's remember our log from our confirm
01:27:14.239 controller down there that only showed up for our Hotwire native app, right?
01:27:21.600 How does it know that we can deal with the confirm? If you look at the HTML element, we have datab bridge platform
01:27:28.719 equals iOS and datab bridge components equals confirm. I didn't write this.
01:27:33.920 This is not coming from the Rails app. This is coming from the Hotwire native
01:27:39.120 code on the iOS app that's injecting that JavaScript onto the HTML element.
01:27:44.800 If we comment out confirm component here. So we're no
01:27:51.440 longer registering any bridge components and then we debug again.
01:28:02.960 We don't have anything. We haven't registered any components. So we don't have the HTML of the platform or the
01:28:09.120 components that we dealt with. So this is how we can add new components in the future and our old apps will still work
01:28:16.480 because usually when you add a component you hide the HTML element behind it. Now if we add a confirm super component in
01:28:24.000 the future the existing apps will only deal with the confirm component and won't get any wires crossed. Kind of
01:28:29.679 like a versioning API. Okay,
01:28:35.280 let's actually do something. We're going to override the onreceive method in the
01:28:41.920 bridge component subclass and we're going to do something here. We're going to print out the message dot event.
01:28:50.159 Print will come down into our console logs right here. On receive is called anytime the stimulus controller passes a
01:28:56.800 message to the bridge component, the native bridge component.
01:29:08.080 Back in our on our rail side on the connect, we're going to do this.
01:29:16.000 Hello or connect with an empty array and then an empty call back.
01:29:23.280 This send is one of the things that bridge components add to stimulus controllers to communicate to the iOS or
01:29:29.440 Android client. Three parameters. The first one is the event of the message. So we're printing that here. The second
01:29:35.600 one is an optional hash or object in JavaScript of arguments. And the third one is a callback function. On iOS, we
01:29:42.880 can always on Android we can say reply to message and then this callback would be
01:29:49.520 executed. So that's where we can start interacting with elements. We can start clicking things in our DOM. Um, it's
01:29:55.440 also optional if you don't want to do it, but for now, let's run this.
01:30:03.920 We're going to click the show button or click the show page
01:30:12.239 and you'll see that we have logged behind connect because of our printing of the
01:30:20.000 message.event. So this is this is strata. This is what
01:30:26.320 strata launched with if anyone remembers that a way to communicate from your server to the iOS or Android app. It
01:30:32.960 doesn't do anything. It just is a a communication protocol so to speak. And then strata was like cool you're off.
01:30:39.440 You're off on your own you know go for it. Uh Bridge Components added a little bit more in there where you can start
01:30:44.480 replying to messages. But if you can understand this flow,
01:30:50.239 you can write any bridge component you need with whatever native code you want. Right now, all we're doing is logging
01:30:55.440 something, right? Not very exciting, but you're logging something on the native
01:31:01.199 platform. That's the big difference. You've done it through native code. So, let's make it actually interesting here.
01:31:08.000 Let's grab a view controller from the delegates destination and casting it as
01:31:13.360 a UI view controller, right?
01:31:21.280 Then we'll have to import UI kit.
01:31:28.719 Whoa. Lots of times u we have now said hey delegate which is part of bridge
01:31:34.639 component give me your destination also if it only give it to me as a UI view controller because I want access to a UI
01:31:40.800 view controller to start popping things on the screen. This is going to be an optional, so we'll have to deal with it with question marks. We're going to
01:31:47.520 create an alert, a UI alert controller, which we're going to give a title of um
01:31:53.360 are you sure? A no message, and a preferred style of alert.
01:32:02.960 And then we're going to say on that view controller, we're going to present
01:32:08.159 this alert animated
01:32:16.960 So, we grabbed a reference to our view controller via the delegate destination. Don't forget this question mark and that
01:32:23.120 question mark. We've created an alert that says, "Are you sure?" We want the OS to display it as an alert versus a
01:32:29.760 confirm or sorry versus a action sheet. And then we're going to present that on top of the view controller with
01:32:35.199 animation. When we run this
01:32:48.719 and we go to the show page, we get our native alert.
01:32:53.920 So, we've now bridged the gap. We're doing something here with UI kit to get
01:32:59.600 a native element on top of the screen. A native element we can't get rid of, but
01:33:04.639 that's a different question. Um, I'm clicking here. you know, this is just this is the app now. You're stuck here forever. Uh, but we we we presented a
01:33:12.000 native screen, right? We presented a native element on top of that screen. Let's make title dynamic, right?
01:33:20.719 So over here on the left in this, we're going to give a const title and we're going to call it um, you know,
01:33:29.600 are you really sure? And that is then we're going to pull
01:33:34.719 that out of the message. So, we're going to pass it down here.
01:33:48.719 So, those not familiar with this syntax is we're getting a hash or an object in in JavaScript of title colon value
01:33:55.360 title. It's just a shortand for it. It's the same as if we had did
01:34:00.560 uh that. And then down here, we want to extract that title from our message. So, we're
01:34:09.040 going to create a strct. It's going to be a uh message data and it's going to be a
01:34:18.400 decodable. And this is going to have a name called
01:34:24.159 title, which is a string.
01:34:31.120 Decodable is Swift's interface to getting stuff from JSON, decoding it
01:34:37.600 into objects. Encodable can take objects and turn it into JSON. We can take
01:34:42.639 primitives like a string and get serialization and des serialization entirely for free.
01:34:49.440 We're going to use this message data strct to extract the title from our message data.
01:34:56.480 So up here where we have our view controller, we're also going to grab our
01:35:01.840 data equals as message data. So we're like we're cast we're giving it a type here
01:35:09.120 and say message data else return.
01:35:17.520 So here's that guard statement that unless statement we're saying hey if you can get call this data function from um
01:35:25.040 hotwire native if you can cast that as one of these instances
01:35:30.320 and assign it here keep going if you can't bail something went wrong you know
01:35:37.840 insert developer to-do error message there right uh and then we have data we
01:35:43.360 want to pass it into title so here we say title data.title
01:35:49.760 because now I'm not going to even title. Okay,
01:35:56.480 let's run this.
01:36:04.400 Go to the show page and we have our new are you really sure title from our web.
01:36:11.679 So we're now passing data down the wire and we hardcoded are you really sure
01:36:16.880 into our JavaScript. Sure, we could pull that out, right? We can grab this from a
01:36:22.000 static uh values
01:36:28.639 and then say on the show page um
01:36:35.120 bridge confirm title value
01:36:44.239 and then over here grab the title from this title.
01:36:50.800 value, right? We could do that. And then let me I actually wouldn't have had to
01:36:57.040 rerun the app if I didn't have us in a blocking situation. Railscom comp. So now we're getting it not only from the
01:37:03.600 JavaScript, but we're getting from the HTML. And if we can get it from the HTML, we can get it from Ruby, right?
01:37:09.199 This could be an ERB. This is the ERB. This could be the name of the user, the a URL, an API key. Uh, all of this stuff
01:37:16.800 is now completely dynamic. This doesn't know anything. Sorry, the
01:37:22.639 iOS code doesn't know anything about what the content is. It only knows the
01:37:29.119 structure of how to present it. And that's really powerful.
01:37:34.639 Last thing on this is we're going to reply to this message. We're going to grab uh we're going to do grab the alert
01:37:39.760 and say add action. and we're going to give it an UI alert action called uh yes, and that's going to be
01:37:46.880 the destructive. And we're going to add another one,
01:37:52.159 and say cancel, which is going to be cancel.
01:37:57.920 So, I've added those two buttons. Confirm is a destructive one, which should give us a nice red background.
01:38:03.840 And cancel is the bolded one because it's like, hey, if you want to back out, always click bold. You can only have one cancel. And then when I click one of
01:38:10.560 those, nothing happens. We have an answer to get. We can add a handler to each of these.
01:38:19.840 We only care about the destructive action, right?
01:38:26.159 So there's a handler. We have a callback. The underscore is a variable that we don't care about. We're going to
01:38:31.199 print out confirmed. And when we do this,
01:38:36.719 our logs, when we click confirm, we get our confirmed down all the way at
01:38:41.840 the bottom of there. But what we really want to do is reply to the message
01:38:48.880 event. self.rely to message event. What this is
01:38:54.480 doing is calling this callback right here.
01:39:05.199 So here we're now back in JavaScript world. Okay, so this component doesn't
01:39:11.440 really do anything right now, right? It's just like presenting this onto the screen right away. So I'm going to change the connect to uh confirm. And
01:39:21.920 what we're going to do is grab the show page and instead of doing a controller, we're going to do a bridge confirm.
01:39:30.080 Thank you. Data action bridge confirm confirm. And so when we click this, it's
01:39:37.679 going to actually fire this.
01:39:43.360 And here we are going to let's see what's the best way to do this button to
01:39:50.639 confirm uh post and we're going to give it a
01:39:56.000 data turbo action data turbo
01:40:19.119 So, we've wired everything up, but we're preventing the default. So,
01:40:24.639 every time we click up.
01:40:38.320 Yes, we can do that. But we would never be able. We actually do want to click this link
01:40:45.040 when they confirm it. So if we added it to the HTML, we'd never be able to click it. So here we're saying
01:40:54.320 uh the first time they click it, we're going to say clicked equals true and then prevent the
01:41:00.480 default. And here we're going to say, wait, no, my logic always gets backwards here.
01:41:07.119 This dot Yes. Uh, this dot clicked equals true.
01:41:23.280 And then
01:41:31.840 I think that should be it. We only want to prevent the default the
01:41:37.199 first time they click it. The second time they click it, we're the one who is
01:41:43.040 clicking the element, right? We're the one who's saying as JavaScript, click
01:41:48.400 this element because they've confirmed it.
01:41:56.400 Oh, this click doesn't exist yet.
01:42:02.000 All right, JavaScript experts here. Help me. I need this to exist when this is initialized.
01:42:08.639 Thank you. Okay, thank you.
01:42:15.840 So, delete this post, we get the confirmation dialogue. And when I click
01:42:21.440 confirm,
01:42:28.880 let's debug. What's going on here?
01:42:33.920 So we can set a break point in our JavaScript with debugger on confirm.
01:42:41.600 Refresh this page to get the new content. Click delete this post.
01:42:49.040 And we now have our we're in our debugger in JavaScript,
01:42:55.520 right? This clicked is false. So if
01:43:03.600 I need this one, not this one. If I step over that, if this clicked is false,
01:43:08.800 preventing default, and then we're doing this, and then we're going to hit run. Now, when I click
01:43:16.960 confirm, we're going to call down here. So I'll set a breakpoint manually. There
01:43:23.040 we're in this section here. This clicked is still false. We step over that.
01:43:32.800 We get it to true. This element is going to call confirm again.
01:43:45.440 Yeah. Confirm. Confirm.
01:43:57.119 uh doesn't matter. We're replying to the same thing in iOS by passing message.event.
01:44:07.119 So whatever we're getting over the wire there, we're still just getting sending it back to um so what's going on here?
01:44:15.440 So we didn't prevent default. Oh, you know what's wrong?
01:44:23.119 We uh this this syntax isn't right. data turbo action delete. Is that right?
01:44:29.360 To delete a post turbo method, right? Yeah.
01:44:41.920 Okay. This is backwards. Okay.
01:44:50.880 So now delete this post.
01:45:13.280 Um, not sure what's going on there.
01:45:20.880 Second,
01:45:27.760 but then this should fire. Oh, this is backwards.
01:45:46.080 I got too fancy with my refactor. Okay, there we go. If it hasn't been
01:45:52.880 clicked yet.
01:45:58.639 Also true. Thank you.
01:46:04.880 All right, delete this post.
01:46:10.320 Cancel doesn't do anything because we're not responding to that event. Delete this post. Confirm
01:46:17.199 is making a network request to our server
01:46:26.320 with patch. We want it to be delete.
01:46:32.080 So, isn't this
01:46:37.760 should be inside the data, right? Yeah.
01:46:43.840 Cool. Refresh the page. No changes to our iOS app. Delete this post. Confirm. Oh my
01:46:50.960 god. Missing param.
01:47:01.679 It's still patching.
01:47:11.840 I'm crossing wires with link two.
01:47:21.040 It's funny because we're not actually uh doing turbo native here or hotware native. It's just Rails.
01:47:30.320 Hey, we deleted it. Oh my god. Just like that.
01:47:39.840 Okay, four minutes. Awesome.
01:47:45.119 There's a lot going on here. I recognize that. A lot of it though was not the iOS
01:47:51.920 code. It was us fussing around with hotwire on the web and and getting my data attributes right and getting the
01:47:57.119 path right and method delete and all that. What we've built here though is a
01:48:02.320 dynamic way to create a native uh sorry a native confirm dialogue for any action
01:48:07.679 across our app. We can drop this HTML on the posts index page, right? And say,
01:48:16.880 uh, visit the dark web and link to
01:48:22.880 Google, right? And we don't want any of that. And now on our homepage with no no changes to
01:48:31.040 our native app, we're visiting Google, right?
01:48:39.280 We visited through JavaScript, so Google's freaking out on us. But the point is that we just added that button,
01:48:44.560 that native confirm button to a new screen by copying pasting seven lines,
01:48:50.320 five lines of HTML. So we can now drop this on the profile page and say, hey,
01:48:55.760 are you sure you want to delete your account? Yes, you know, delete your account. Are you sure you want to delete this image? We can also start to make
01:49:02.239 the confirm component. If we say let destructive
01:49:10.239 boolean that's defaulted to true, right? Or don't default that to true. We can
01:49:15.840 now start to say is it destructive and grab that dynamically and well and now our confirm component is just a plain
01:49:22.159 old alert component that has the option to do destructive actions. We can start
01:49:27.920 making these more and more advanced by adding layering in these different properties or variables from the message
01:49:34.719 data that we're getting from our server. Look at you. One sec. Um, the only thing
01:49:40.480 that I will say with all of these is you want to keep these as generic as possible. You very rarely want a profile
01:49:47.760 photo component. You very rarely want a post component. You want things like
01:49:53.040 confirm. You want things like alert. You want things like notification token. You want them to relate to a native API one
01:49:59.840 to one. Usually a contacts component. If you're looking for more inspiration,
01:50:05.600 you can go to bridgecomponents.dev. And I have a free package of uh 15
01:50:12.880 bridge components that you can copy and paste into your apps both for iOS and
01:50:18.000 Android. Things like the alert component we just built, but a little bit more bulletproof with some error handling. uh
01:50:23.679 barcode scanning, buttons, document scanners that will scan PD and turn them
01:50:29.760 into PDFs with your phone using native ML on the device. Uh location components, all of this stuff that you
01:50:36.080 it's just a GitHub repo. Bridge components.dev is the uh is the
01:50:41.600 URL for that. And you just take these, copy paste them into your app, wire up the stimulus controller, and you now
01:50:46.880 have these native functionality in just a few minutes. Question. Yeah, I would I would say
01:50:53.360 always lean into the hot wire native mindset of keeping as much logic on your
01:50:58.400 server until you absolutely need something. So this example is a little contrived, but if you're talking about
01:51:03.600 notifications, you need the notification token to send them, definitely reach for a bridge component. But as you're you're
01:51:09.760 in version two or three of the app in the app store and you're like, "Hey, let's like build something that feels even better." Then bridge components
01:51:15.840 make more sense. I have 30 seconds left, so I'm going to quickly wrap up before.
01:51:26.000 Yeah, do Android. Um, okay. So, where do you go from here? I wrote a book on
01:51:32.080 Hotwire Native. I'm going to be at the author's table this afternoon. If you want to talk about the book, come say hi. I have discount codes rails comp Joe
01:51:39.520 for 40% off my book through the prague website. Um, which makes it, I think,
01:51:44.960 pretty affordable. bridgecomponents.dev is the component library that I just uh
01:51:51.599 showed you. And then my newsletter is on masalotti.com. I send out a weekly
01:51:56.719 newsletter for hotwire native tips and tricks, how to build your app. Uh links to everything right there in a QR code.
01:52:03.280 And I make my money by consulting. So if you need help with your Hotwire native app, come talk to me. I'd love to help
01:52:10.080 you bring your Rails app to the App Store or Google Play. uh whether I'm working with you and building it or
01:52:15.840 advising you on best practices. That's my talk. I'm Joe. Thank you all
01:52:21.040 for coming and I hope you build something great.
Explore all talks recorded at RailsConf 2025
+77