Turbo Native for iOS

Episode #286 by David Kimura

Summary

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

Resources

Download Source Code

Summary

# layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Template</title>
    <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' %>
  </head>
  <body>
    <div class="container">
      <% unless mobile? %>
        <%= link_to "Home", root_path %>
        <%= link_to "Posts", posts_path %>
      <% end %>
      <%= yield %>
    </div>
  </body>
</html>

# application_helper.rb

module ApplicationHelper
  def mobile?
    request.user_agent.include?('DriftingRubyiOS')
  end
end

# config/development.rb

config.hosts = nil

iOS Code


# swift package url
https://github.com/hotwired/turbo-ios

# 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
        session.visit(viewController)
    }
    
    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:
            home()
        case 1:
            visit(url: URL(string: "https://35546c597679.ngrok.io/posts")!)
        default:
            break
        }
    }
    
    func home() {
        visit(url: URL(string: "https://35546c597679.ngrok.io")!)
    }
}

# WebViewController.swift

import UIKit
import Turbo

class WebViewController: VisitableViewController {
    override func viewDidLoad() {
        super.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() {
        super.viewDidLoad()
        tabBar.frame = CGRect(x: 0, y: self.view.frame.height - 75, width: self.view.frame.width, height: 49)
        tabBar.items = [itemHome, itemPosts]
        self.view.addSubview(tabBar)
    }
}