Hotwireのturbo-iosを触る

October 9, 2021

Rails 7のalphaが出たのをきっかけにHotwireを触っている。 (Hotwireが出た当時は動画などを見るだけで触っていなかった)

Turbo NativeのiOS版が意外と(?)ちゃんと動いており興味を持ったので、 少しだけ中身を見てみた。

準備

サーバー側はrails newして適当にページを作っておく。 (Railsじゃなくてもいいけど慣れてるし楽なので)

1
2
3
4
$ rails -v
Rails 7.0.0.alpha2

$ rails new myapp -j esbuild

-j esbuildがない(JSをbundleせずにImport mapsを使う)とiOSシミュレータで動かなかったので付けた。

クライアント(iOS)を動かすための最低限のコードはこちらにある

https://github.com/hotwired/turbo-ios/blob/main/Docs/QuickStartGuide.md

これで画面遷移がNavigationControllerをつかったものになる。

何が起きているのか?

雑にコードを眺めてみる。

SessionDelegate

1
2
3
4
5
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        window!.rootViewController = navigationController
        visit(url: URL(string: "http://localhost:3000")!)
    }    

visitを呼ぶ。

1
2
3
4
5
6
    private func visit(url: URL) {
        let viewController = VisitableViewController(url: url)
        navigationController.pushViewController(viewController, animated: true)
        session.visit(viewController)
    }

sessionのvisit呼ぶ。

Session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public func visit(_ visitable: Visitable, action: VisitAction) {
        visit(visitable, options: VisitOptions(action: action, response: nil))
    }
    
    public func visit(_ visitable: Visitable, options: VisitOptions? = nil, reload: Bool = false) {
        // 省略
                
        let visit = makeVisit(for: visitable, options: options ?? VisitOptions())
        currentVisit?.cancel()
        currentVisit = visit

        visit.delegate = self
        visit.start()
    }
    
    private func makeVisit(for visitable: Visitable, options: VisitOptions) -> Visit {
        if initialized {
            return JavaScriptVisit(visitable: visitable, options: options, bridge: bridge, restorationIdentifier: restorationIdentifier(for: visitable))
        } else {
            return ColdBootVisit(visitable: visitable, options: options, bridge: bridge)
        }
    }

なんやかんやで JavaScriptVisitのstartVisitが呼ばれる

JavaScriptVisit

1
2
3
4
5
    override func startVisit() {
        debugLog(self)
        bridge.visitDelegate = self
        bridge.visitLocation(location, options: options, restorationIdentifier: restorationIdentifier)
    }

bridgeのvisitLocationに続く。

WebViewBridge

1
2
3
4
5
6
7
    func visitLocation(_ location: URL, options: VisitOptions, restorationIdentifier: String?) {
        callJavaScript(function: "window.turboNative.visitLocationWithOptionsAndRestorationIdentifier", arguments: [
            location.absoluteString,
            options.toJSON(),
            restorationIdentifier
        ])
    }

JSの世界へ。

turbo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    visitLocationWithOptionsAndRestorationIdentifier(location, options, restorationIdentifier) {
      if (window.Turbo) {
        Turbo.navigator.startVisit(location, restorationIdentifier, options)
      } else if (window.Turbolinks) {
        if (Turbolinks.controller.startVisitToLocationWithAction) {
          // Turbolinks 5
          Turbolinks.controller.startVisitToLocationWithAction(location, options.action, restorationIdentifier)
        } else {
          // Turbolinks 5.3
          Turbolinks.controller.startVisitToLocation(location, restorationIdentifier, options)
        }
      }
    }

これでTurboにより初回ページが読み込みされる。

↓でregisterAdapterされてるので、ページ内にあるリンクも同様にturbo-ios内のturbo.jsによって処理される。

turbo.js

1
2
3
4
5
6
7
8
9
    registerAdapter() {
      if (window.Turbo) {
        Turbo.registerAdapter(this)
      } else if (window.Turbolinks) {
        Turbolinks.controller.adapter = this
      } else {
        this.pageLoadFailed()
      }
    }

なんとなくわかった(?)のでバグに遭遇しても対処できそうな気がしてきた。