Unable to get a native bridge working with Rails 7.2 and Hotwire Native Bridge 1.0.0

I’m following the iOS Turbo Native guides that are found here

Just trying to connect up their exact example for the button, to ensure there is a proper connection going. It’s not working yet, one thing I’m noticing is when my Stimulus controller inherits from BridgeComponent that I don’t see initialize or connect events happening. I’m curious if this is normal, or if that in indicating where my problem is.

In either case this is roughly my setup.

Backend

Rails 7.2

app/javascript/controllers/application.js

I’ve enabled debugging here so I can see all the events.

import { Application } from "@hotwired/stimulus"

const application = Application.start()

// Configure Stimulus development experience
application.debug = true
window.Stimulus   = application

export { application }

app/javascript/controllers/bridge/button_controller.js

Then I have the bridge javascript component

import { BridgeComponent } from "@hotwired/hotwire-native-bridge"

export default class extends BridgeComponent {
  static component = "button"

  connect() {
    console.log("bridge button connected")
    super.connect()
    const element = this.bridgeElement
    const title = element.bridgeAttribute("title")
    this.send("connect", {title}, () => {
      this.element.click()
    })
  }
}

app/views/layouts/application.html.erb

I’m connecting it up to my button in my main layout.

<button type="button" data-controller="bridge--button" data-bridge-title="Profile">

iOS

ButtonComponent.swift

I added the ButtonComponent using code from here

import HotwireNative
import UIKit

final class ButtonComponent: BridgeComponent {
    override class var name: String { "button" }

    override func onReceive(message: Message) {
        guard let viewController else { return }
        addButton(via: message, to: viewController)
    }

    private var viewController: UIViewController? {
        delegate.destination as? UIViewController
    }

    private func addButton(via message: Message, to viewController: UIViewController) {
        guard let data: MessageData = message.data() else { return }

        let action = UIAction { [unowned self] _ in
            self.reply(to: "connect")
        }
        let item = UIBarButtonItem(title: data.title, primaryAction: action)
        viewController.navigationItem.rightBarButtonItem = item
    }
}

private extension ButtonComponent {
    struct MessageData: Decodable {
        let title: String
    }
}

SceneDelegate.swift

And then linked it up in the SceneDeletegate

import HotwireNative
import UIKit

let rootURL = URL(string: "https://my.domain")!
let localPathConfigURL = Bundle.main.url(forResource: "path-configuration", withExtension: "json")!

let pathConfiguration = PathConfiguration(sources: [
  .file(localPathConfigURL),
])

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    private let navigator = Navigator(pathConfiguration: pathConfiguration)

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        window?.rootViewController = navigator.rootViewController
        
        Hotwire.registerBridgeComponents([
            ButtonComponent.self
        ])
        
        navigator.route(rootURL)
    }
}

Output

Starting it up I’m noticing three clear things.

  1. There is no button rendered in the iOS app
  2. The stimulus debugging doesn’t show initialized or connected for the bridge–button controller.
  3. The javascript console doesn’t show my console.log when connecting.

Now what’s interesting is I can swap out BridgeComponent in the JavaScript with the typical stimulus Controller and I start to see events. (Of course the button still doesn’t show, but I’m sure that’s because that controller doesn’t know how to connect.)

I thought maybe I don’t have recent versions of the javascript dependencies, but I believe I’m pretty much up to date.

  • turbo-rails 8.0.10
  • stimulus 3.2.2
  • hotwire-native-bridge 1.0.0

Is there anything I’m missing here?

I got this working, but it required changes on both sides.

Backend

On the backend it was necessary to import the library early in the process. When you import the library it’s creating the Strada object.

app/javascript/application.js

import "@hotwired/turbo-rails"
import "@hotwired/hotwire-native-bridge"
...

Frontend

The same demo is different than the example, it’s using a SceneController instead of a SceneDelegate. Once I broke it down, the key issue seemed to be that the navigator line needed to delegate itself.

That required two things

  1. Add an extension on SceneDelegate for NavigatorDelegate
  2. Add an extra param to the Navigator initializer for delegate to self
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    ...

    private lazy var navigator = Navigator(pathConfiguration: pathConfiguration, delegate: self)

    ...
end

extension SceneDelegate: NavigatorDelegate {
}