Turbo Native for iOS

Episode #286 by David Kimura


In this episode, we look at modifying our Rails application and building a hybrid native application for iOS using Hotwire.
ios rails turbo hotwire 22:07


Download Source Code


# layouts/application.html.erb

<!DOCTYPE html>
    <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <%= stylesheet_link_tag 'application', media: 'all' %>
    <div class="container">
      <% unless mobile? %>
        <%= link_to "Home", root_path %>
        <%= link_to "Posts", posts_path %>
      <% end %>
      <%= yield %>

# application_helper.rb

module ApplicationHelper
  def mobile?

# config/development.rb

config.hosts = nil

iOS Code

# swift package url

# SceneDelegate.swift

import UIKit
import Turbo
import WebKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    private lazy var navigationController = ViewController()
    let viewController = WebViewController()

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        window!.rootViewController = navigationController
        navigationController.tabBar.delegate = self
        navigationController.pushViewController(viewController, animated: true)
        visit(url: URL(string: "https://35546c597679.ngrok.io")!)
    private func visit(url: URL) {
        viewController.visitableURL = url
    private lazy var session: Session = {
        let configuration = WKWebViewConfiguration()
        configuration.applicationNameForUserAgent = "DriftingRubyiOS"
        let session = Session(webViewConfiguration: configuration)
        session.delegate = self
        return session

    func sceneDidDisconnect(_ scene: UIScene) {
        // Called as the scene is being released by the system.
        // This occurs shortly after the scene enters the background, or when its session is discarded.
        // Release any resources associated with this scene that can be re-created the next time the scene connects.
        // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead).

    func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.

    func sceneWillResignActive(_ scene: UIScene) {
        // Called when the scene will move from an active state to an inactive state.
        // This may occur due to temporary interruptions (ex. an incoming phone call).

    func sceneWillEnterForeground(_ scene: UIScene) {
        // Called as the scene transitions from the background to the foreground.
        // Use this method to undo the changes made on entering the background.

    func sceneDidEnterBackground(_ scene: UIScene) {
        // Called as the scene transitions from the foreground to the background.
        // Use this method to save data, release shared resources, and store enough scene-specific state information
        // to restore the scene back to its current state.


extension SceneDelegate: SessionDelegate {
    func session(_ session: Session, didProposeVisit proposal: VisitProposal) {
        visit(url: proposal.url)
    func session(_ session: Session, didFailRequestForVisitable visitable: Visitable, error: Error) {
        print("didFailRequestForVisitable: \(error)")

extension SceneDelegate: UITabBarDelegate {
    func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
        switch(item.tag) {
        case 0:
        case 1:
            visit(url: URL(string: "https://35546c597679.ngrok.io/posts")!)
    func home() {
        visit(url: URL(string: "https://35546c597679.ngrok.io")!)

# WebViewController.swift

import UIKit
import Turbo

class WebViewController: VisitableViewController {
    override func viewDidLoad() {
    override func visitableDidRender() {
        title = "Drifting Ruby"

# ViewController.swift

import UIKit

class ViewController: UINavigationController, UITabBarDelegate {
    let tabBar = UITabBar()
    let itemHome = UITabBarItem(tabBarSystemItem: .favorites, tag: 0)
    let itemPosts = UITabBarItem(tabBarSystemItem: .bookmarks, tag: 1)
    override func viewDidLoad() {
        tabBar.frame = CGRect(x: 0, y: self.view.frame.height - 75, width: self.view.frame.width, height: 49)
        tabBar.items = [itemHome, itemPosts]