LearnRails.Dev
Home / Guides / Building Your First Hotwire Native iOS App

Building Your First Hotwire Native iOS App

min read Last updated: January 08, 2025

Table of Contents

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

If you need a Rails app to follow along with…​

You can clone our Bookmarks application from GitHub:

git clone git@github.com:learnrails-dev/bookmarks.git

The application is setup using Rails 8 and SQLite3, with Hotwire enabled. You can run the Rails server with:

bin/dev

Project Setup

Creating a New Xcode Project

  • Open Xcode

  • Click "Create a new Xcode project" (or File → New → Project)

Xcode Project Select
  • Select "App" under iOS templates

Xcode iOS template selection
  • Configure your project:

Product Name: Bookmarks
Team: Your Name
Organization Identifier: com.example
Interface: Storyboard
Language: Swift
Xcode iOS app configuration

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 dev.learnrails.bookmarks so it’s unique from everyone else.

Adding Dependencies

  1. In Xcode, select File → Add Packages

  2. Click the '+' button in the top-left corner

  3. Enter the Hotwire Native iOS repository URL:

https://github.com/hotwired/hotwire-native-ios
  1. 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.

Note that we’re glossing over a lot of iOS specific details here, mostly because for our app, we’re not worried about running multiple scenes. If you’re curious about this topic, you can read more about it in the official Apple documentation.

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!

iOS app in the simulator

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:

  1. Add a title to the app, so each page can have a different title.

  2. 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.

Note that the AppDelegate.swift file is the entry point for the iOS app, it’s where the app is initialized and where you can add global configuration for the app.

Hotwire Native can load a path configuration locally from the bundled files with the iOS app, and it can also load files remotely from a server, so you can always count on there to be a path configuration available for the app to use, even if the web app is offline or unreachable.

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

  1. Select a simulator from the device menu in Xcode (iPhone or iPad)

  2. Click the "Play" button or press Command + R

  3. 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

Want to Read More?

This is just a preview. Get access to the full course and learn how to build complete web and mobile applications with Rails.

Get Full Access (coming soon!)