Overview
This guide walks you through creating your first iOS app using Hotwire Native, starting from absolute basics. We’ll build a simple bookmarks app that connects to a Rails backend, assuming no prior iOS development experience.
Prerequisites
-
Xcode installed (follow "Setting Up iOS Development Environment" guide)
-
A Rails application with Hotwire enabled
-
Basic understanding of Ruby on Rails
-
macOS computer
Project Setup
Creating a New Xcode Project
-
Open Xcode
-
Click "Create a new Xcode project" (or File → New → Project)
-
Select "App" under iOS templates
-
Configure your project:
Product Name: Bookmarks
Team: Your Name
Organization Identifier: com.example
Interface: Storyboard
Language: Swift
Leave all the checkboxes unchecked and then proceed with the rest of the guide.
PS: It’s okay if you want to use SwiftUI for the views later, we just need select Storyboard to start with so the mobile app will be generated with files we need. We will add custom SwiftUI views later.
Tip
|
The Organization Identifier is typically your domain name in reverse. For example, if your website is example.com, use com.example So for our bookmarks app I used |
Adding Dependencies
-
In Xcode, select File → Add Packages
-
Click the '+' button in the top-left corner
-
Enter the Hotwire Native iOS repository URL:
https://github.com/hotwired/hotwire-native-ios
-
Click "Add Package"
Run the app for the first time
Click Product → Run or press Command + R to run the app in the simulator. You should see a blank screen, really riveting stuff! But you now know that your app is set up correctly and ready to be built upon.
Basic App Structure
First in SceneDelegate.swift
, delete all the existing content and then import the HotwireNative module and UIKit.
import HotwireNative
import UIKit
Also let’s go ahead and add a variable to hold the rootURL of our Rails app:
let rootURL = URL(string: "http://localhost:3000")!
Note that you should replace http://localhost:3000
with the URL of your Rails app. Also keep in mind the ! at the end of the URL, it’s a force unwrap operator that will crash the app if the URL is not valid.
Next up let’s ensure we setup a window and a navigator in the SceneDelegate
class:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private let navigator = Navigator()
}
Note that the window var is optional so we mark that with a ? at the end. Also, the navigator is a new instance of the Navigator class we imported from Hotwire Native.
iOS will set the window property for us after things are initialized so it needs to be optional so we can allow the nil value at first.
Next up let’s add the scene
function to the SceneDelegate
class:
func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
window?.rootViewController = navigator.rootViewController
navigator.route(rootURL)
}
We mark the scene
parameter as _
because we don’t use it in the function body, just like we would with a variable we don’t use in Ruby.
In order for iOS to actually render content, we have to set the root view controller of the window to the navigator’s root view controller. View Controllers are the building blocks of iOS apps and they are used to manage the content of the app. Think of them like a Rails controller + view combined, it’s really nifty!
Finally, we call the route
function on the navigator with the rootURL we defined earlier. This function will load the content from the Rails app and render it in the app.
Now, go ahead and run the app again in the simulator, and if all goes well and you’re running your Rails app locally on port 3000, you should see the content from your Rails app in the iOS app!
Let’s take a brief look at this, the app renders a Navigation Bar at the top with a back button (when you navigate around and a title. Below that is the content from the Rails app. This is the basic structure of a Hotwire Native iOS app.
The title of the Navigation Bar is the page’s HTML title in the Rails app. The back button is automatically added by Hotwire Native when you navigate to a new page, it will handle the navigation stack for you.
The benefits of Hotwire Native
When you choose to write a Hotwire Native app, you’re choosing to write less code, because you can reuse code from the Rails app in the iOS app automatically. When you need high fidelity, you can write custom Swift code to enhance the experience, but for most apps, you can get away with just using Hotwire Native with sprinkles of native features, which is a huge time saver.
Driving change to the native app from the Rails app
Let’s implement a few quick wins for our application:
-
Add a title to the app, so each page can have a different title.
-
Hide the web navigation bar so we don’t duplicate our navigation bar.
Adding a title to the app
In the Rails application, let’s open our application.html.erb
layout file and add a title tag if one isn’t present:
<title><%= content_for(:title) || "Bookmarks" %></title>
Note that we’re using content_for
which is a super useful method in Rails to allow us to inject content into a layout from a view. If the view doesn’t provide a title, we default to "Bookmarks".
Next let’s add a title to our bookmarks/index.html.erb
page:
<% content_for :title do %>
My Bookmarks
<% end %>
Now go back to your simulator and click and drag downwards to refresh the content. You should see the title "My Bookmarks" at the top of the app. Perfect!
Go ahead and take a moment and add a title to the other pages in your Rails app if one isn’t set. For me I’ve added one to the new bookmarks page.
<% content_for :title do %>
New Bookmark
<% end %>
Okay let’s move on to the next thing
Path Configuration
The current state of our app is enough to function, but it’s not ideal. We’re able to improve the user experience with just a little bit of additional work and configuration.
Look at the process for creating a new bookmark, it opens a page in the web view, but what if we want it to show up in a 'modal' instead? In iOS this is common for tasks which are focused, where they open in a 'sheet' above the current content, then when you submit them the sheet disappears and the page is updated.
We can achieve this by adding a path configuration to our Rails app. This is a JSON file that tells the iOS app how to handle certain paths, like opening them in a modal or the current view.
{
"settings": {},
"rules": [
{
"patterns": [
".*"
],
"properties": {
"context": "default",
"pull_to_refresh_enabled": true
}
},
{
"patterns": [
"/new$"
],
"properties": {
"context": "modal",
"pull_to_refresh_enabled": false
}
}
]
}
Let’s break this down, we’ve created a JSON object with a settings key and a rules key. The settings key is for global settings for the app, and the rules key is for specific rules for certain paths. The rules key is an array of objects, each object has a patterns key which is an array of regular expressions to match the path, and a properties key which is an object with the context key which is the context to render the path in, and the pull_to_refresh_enabled key which is a boolean to enable or disable pull to refresh for the path.
In the case of this, we’re setting the default context to be the default context, and enabling pull to refresh for all paths. For the /new path, we’re setting the context to be modal, and disabling pull to refresh, since they’re in a modal pulling to refresh would just close the modal anyway and result in weird touch interactions.
Using the Path Configuration in the iOS app
Now we have a JSON file, we need to update the iOS app to use it. Let’s add some additional configuration in the AppDelegate.swift
file to load the path configuration.
We configure Hotwire in the AppDelegate file to ensure that it’s always configured before the first URL is routed to.
import HotwireNative
import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let localPathConfigURL = Bundle.main.url(forResource: "path-configuration", withExtension: "json")!
Hotwire.loadPathConfiguration(from: [
.file(localPathConfigURL),
])
return true
}
}
Next for the Bundle
method, we need to add the path-configuration.json
file to the Xcode project. To do this, right-click on the Bookmarks
folder in the Xcode project navigator, select "New File", and then select "Empty" from the list of templates. Name the file path-configuration.json
and click "Create".
Next, let’s go ahead and restart our simulator since we’ve added new code. Click the stop button in Xcode, then click the play button to run the app again.
Testing
Now when we load the app, open any page that ends with /new then the resulting page will show up in a sheet!
Great success!
Testing Your App
Running in Simulator
-
Select a simulator from the device menu in Xcode (iPhone or iPad)
-
Click the "Play" button or press Command + R
-
Test the following functionality:
-
Modal presentation for new bookmark creation
-
Pull-to-refresh functionality
-
Network error handling
-
Back navigation
-
Common Issues and Solutions
App Shows Blank Screen
-
Verify your Rails server is running and accessible
-
Check that rootURL matches your Rails application URL
-
Ensure your network permissions are properly configured in Info.plist
-
Verify SSL certificate settings for production environments
Navigation Not Working
-
Confirm URL formats in path configuration
-
Verify Turbo is properly configured in your Rails application
Development Best Practices
Code Organization
-
Keep your Swift extensions in separate files
-
Group related files in appropriate folders (Models, Controllers, Extensions)
-
Use consistent naming conventions across your codebase
Error Handling
-
Add proper error handling for network requests
-
Implement user-friendly error messages
-
Consider offline functionality
Performance
-
Minimize network requests
-
Cache responses when appropriate
-
Consider implementing loading states
Security
-
Use HTTPS in production
-
Implement proper certificate handling
-
Consider app transport security settings
Debugging Tips
Common Debugging Scenarios
Network Issues
-
Use the Network Inspector in Xcode
-
Add logging for network requests
-
Check your Rails server logs
UI Issues
-
Use the View Hierarchy Debugger in Xcode
-
Add debug prints for navigation events
-
Test on different device sizes
Useful Development Tools
-
Xcode Console for logs
-
Safari Web Inspector for web content
Conclusion
You’ve successfully created your first Hotwire Native iOS app! This foundation provides you with:
Technical Achievements
-
Basic iOS app architecture understanding
-
Hotwire Native integration knowledge
-
Swift fundamentals
-
Path configuration implementation
Next Steps
-
Add authentication
-
Implement custom native features
-
Add offline support
-
Enhance the user interface
-
Add push notifications
Best Practices to Remember
-
Always test on multiple iOS versions
-
Consider accessibility features
-
Implement proper error handling
-
Keep your dependencies updated
-
Follow iOS design guidelines
Tip
|
Remember to: * Test your app on different iOS devices and orientations * Verify behavior with slow network conditions * Check memory usage with Instruments * Review Apple’s Human Interface Guidelines |