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 {
}

Hmm, I think I’m having the same issue. Followed the guides, still not getting a native button active. I did end up adding the import in my application.js and I do see window.Strada is defined. I did also add the delegate option to the Navigator in SceneDelegate and I’ve used it to add a native view following some examples, but the bridge components aren’t working for me.

One thing I’m wondering is, how do we see the JS console from a Hotwire Native app? If I load the app in a normal browser I can of course see the logs, and if I switch back to a regular controller instead of a BridgeComponent I do see errors and messages that indicate that it was connected. But when I inherit from BridgeComponent nothing seems to happen.

Ahhh, the bug I ran into was this: Bridge components aren't always registered · Issue #35 · hotwired/hotwire-native-ios · GitHub

I still wonder if there’s a way to see the JS console from the mobile device/simulator.

This is (I think) a mistake in the documentation.
If you look at BridgeComponent hotwire-native-bridge/src/bridge_component.js at main · hotwired/hotwire-native-bridge · GitHub you see it will not load if it thinks it is not in a Hotwire app.
The way it decides that is if the userAgent has any bridge-components.

I think that the userAgent is established when the rootViewController is first accessed. So in the example on the website it access rootViewController before registering bridge components so the userAgent is not populated correctly.

PR to the docs Fix small error in iOS documentation by chrisortman · Pull Request #59 · hotwired/hotwire-native-site · GitHub

This is not limited to Rails 7.2. I hit this on a fresh 8.0 app.