Rails: Hotwire Nativeでネイティブモバイルアプリを作ろう: iOS編パート1(翻訳)
私のブログ記事で人気があるシリーズは、Turbo Nativeの導入と実行に関するものでした。私は、純粋にビジネスの観点からこのライブラリを熱烈に愛しています。専任のネイティブアプリチームを招集せずに済み、モバイルプラットフォームごとに構築する画面を同じにするための苦労が不要になるので、時間とコストを大幅に節約できます。
Rails World 2024の開催直前にこの方面でエキサイティングな進展が見られました。37signalsと、あの伝説のJoe Masilottiが、Hotwire Nativeをリリースしたのです。
Hotwire Nativeは、開発者の人間工学の観点から、従来のturbo-native-iosライブラリとturbo-native-androidライブラリから完全な進化を遂げ、Webアプリにネイティブコンパニオンアプリを設定するのがこれまで以上に簡単になりました。
そこで、本チュートリアルシリーズ記事では、iOSから始めてAndroidへと進める形でHotwire Nativeについて詳しく見ていくことにします。
🔗 背景(読み飛ばしても構いません)
読者のほとんどはHotwireについて既にひととおりご存知だと思いますが、手短に背景を説明しておきます。
Hotwireは、「Web」「iOS」「Android」を対象とするライブラリのコレクションで、サーバー側でレンダリングするHTMLをマークアップする際の人間工学を重視しています。
Hotwireの方法論は、ReactなどのSPA(シングルページアプリケーション)フレームワークがHTMLをオンザフライで生成し、JSONエンドポイントからの入力に基づいて状態の変更に「反応(react)」することが多いのとは対照的です。
Hotwireの中核にあるturbo.jsというライブラリは、ブラウザのページ全体を再読み込みする必要も、キャッシュなどの追加機能の実行も必要もなしに、ページナビゲーションを処理します。
turbo.jsのようなライブラリを利用するメリットは、SPAについて回る開発上の欠点を取り除いて、SPAのさまざまな長所を得られることです。
🔗 Railsアプリをセットアップしてみよう
Hotwire Nativeはturbo.jsと共生関係にあるので、ページナビゲーションをturbo.jsで処理しているアプリなら問題なく利用できます。
turbo.jsは任意のバックエンドフレームワークで利用可能ですが、Railsではデフォルトで付属しています。
アプリがまだない方は、さくっと作ってみましょう。本記事ではRails 7.2以降を利用することを前提としています。
rails new hotwire_native_todo
scaffoldで手早く作成を済ませましょう。
rails g scaffold Todo title:string complete:boolean
マイグレーションも実行します。
rails db:migrate
それではToDoのindexページをhomeページに設定しましょう。
config/routes.rbファイルにroot "todos#index"
を追加します。
# config/routes.rb
Rails.application.routes.draw do
resources :todos
root "todos#index"
end
最後に、アプリケーションレイアウトにsimplecssを追加してスタイルを手早く整えましょう。
<!-- app/views/layouts/application.html.erb -->
<!DOCTYPE html>
<html>
<head>
...
<link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css">
</head>
<body>
...
それではRailsサーバーを起動して、いよいよiOSアプリに取り掛かることにしましょう。
bin/rails s
ブラウザでlocalhost:3000
を実行すると、できたてのRailsアプリが表示されるはずです。
🔗 iOSアプリをセットアップする
iOSアプリのセットアップには、いくつかの手順を実行する必要があります。
- 1. Xcode を開き、「File」→「New」→「Project」で新しい iOS アプリを作成します。
このとき、SwiftUIではなく、必ず「Storyboard」を選択すること。
2. プロジェクトの保存場所を指定して「Create」をクリックします。
🔗 Hotwire Nativeに統合する
次に、「File」 → 「Add Packages Dependencies」Hotwire Nativeパッケージを追加し、検索フィールドにhttps://github.com/hotwired/hotwire-native-ios
と入力します。プロジェクトが「Add to Project」で正しく設定されていることを確認したら、「Add Package」をクリックします。
パッケージのダウンロードが完了したら、「Add to Target」列の下のドロップダウンで、作成したアプリ名を選択して、「Add Package」をクリックします。
最後に、SceneDelegate
ファイルを開いて、内容を次のコードにすべて置き換えます。
import HotwireNative
import UIKit
let rootURL = URL(string: "http://localhost:3000")!
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
private let navigator = Navigator()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
window = UIWindow(windowScene: windowScene)
window?.rootViewController = navigator.rootViewController
window?.makeKeyAndVisible()
navigator.route(rootURL)
}
}
次は、localhostからのトラフィック受信を許可する設定をアプリに追加しましょう。
Info.plist
を開いて画面を右クリックし、「Add Row」をクリックします。
ドロップダウンで「App Transport Security Settings」を選択します。次に「+」をクリックして、ドロップダウンで「Allow Arbitrary Loads」を選択し、この項目をYESに設定します。
次に、「Application Scene Manifest」>「Scene Configuration」で"Storyboard name"を削除します。
変更後のInfo.plistは以下のようになります。
これで、Runボタンをクリックするかコマンド+Rキーを押して、できたてのiOSアプリを起動すれば、サーバーサイドHTMLがすべて表示されているのが見えるはずです。
🔗 パスを設定してもう少しネイティブらしくする
Hotwire Nativeでは多くの機能をすぐに利用できますが、さらに一歩先に進むことも可能です。
/new
や/edit
で終わるルーティングの各フォームに、適切なモーダルを表示してみましょう。こういうときは、パス構成(Path Configuration)を導入するのがベストです。
パス構成とは、ネイティブアプリまたはサーバー(またはその両方)に存在するファイルで、特定のコンテンツをネイティブアプリでどのように表示するかを指示できます。
path-configuration.json
という新しいファイルを作成して、以下の内容を追加します。
{
"settings": {},
"rules": [
{
"patterns": [
".*"
],
"properties": {
"context": "default",
"pull_to_refresh_enabled": true
}
},
{
"patterns": [
"/new$", "/edit$"
],
"properties": {
"context": "modal",
"pull_to_refresh_enabled": false
}
}
]
}
次に、Navigatorクラスでパス構成ファイルについて指示します。AppDelegate
ファイルの内容を以下に置き換えます。
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
}
// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
// Called when a new scene session is being created.
// Use this method to select a configuration to create the new scene with.
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
// Called when the user discards a scene session.
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
}
}
アプリを再度ビルドして実行(コマンド+Rキー)すると、/new
や/edit
で終わるルーティングに移動するたびにモーダルポップアップが表示されるはずです。
次回は、iOSアプリにネイティブ画面を追加する方法について解説します。
概要
原著者の許諾を得て翻訳・公開いたします。
日本語タイトルは内容に即したものにしました。
従来Turbo NativeとStradaと呼ばれていたものは、現在はHotwire Nativeに統合されました。
参考: Hotwire Native: Hotwire Native is a web-first framework for building native mobile apps.