Hotwire Native

Building a Native sharing component with Hotwire Native (iOS)


One of the great things about building with Hotwire Native is being able to easily tap into native functionality. A common use case in mobile apps is the ability to share content using the native share functionality. It’s the most center button in Safari, after all. Let’s look at how we can implement this using Hotwire Native’s bridge components! The implementation consists of three parts:

  1. A Stimulus bridge component that handles the web interface.
  2. A Swift bridge component that opens the native sharing modal.
  3. HTML markup to wire everything up.

Let’s break down each piece:

The Stimulus bridge controller

// app/javascript/controllers/bridge/share_controller.js
import { BridgeComponent } from "@hotwired/hotwire-native-bridge";

export default class extends BridgeComponent {
  static component = "share";

  open() {
    this.send("open", { url: window.location.href });
  }
}

This simple Stimulus controller doesn’t need to do much. It defines the bridge component’s name and an open function, which sends a message to the native layer with the current page URL as it’s data payload. Since we only need to send some data one way (to the bridge layer) when the open action is performed, we do not need to override the connect() function or define a callback function.

The Bridge component

// ShareComponent.swift
import HotwireNative
import UIKit

final class ShareComponent: BridgeComponent {
    override static var name: String { "share" }

    override func onReceive(message: Message) {
        guard
            let data: MessageData = message.data(),
            let url = URL(string: data.url)
        else { return }

        openShareModal(url: url)
    }

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

    private func openShareModal(url: URL) {
        let activityViewController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
        viewController?.present(activityViewController, animated: true)
    }
}

private extension ShareComponent {
    struct MessageData: Decodable {
        let url: String
    }
}

The Swift component handles the native part of the implementation. When it receives a message from the share component, it creates a UIActivityViewController - iOS’s native sharing interface - with the url from the data payload, and opens it by passing it to the viewController. We only have one possible event (open), so there is no need to check for specific message types in the onReceive function and we can just assume it will always be that event.

There are some additional options to pass to the UIActivityViewController initializer, you can for example exclude certain activity types or sections. Check out the documentation for more information.

Tying it all together: the HTML part

<div data-controller="bridge--share">
  <button data-action="bridge--share#open">Share</button>
</div>

Implementing the HTML markup is straightforward. We add a button that triggers the open action on the bridge--share controller when clicked.

You can also combine this with other Hotwire Native components, for example inside a menu, because those will simply trigger a click event, which in turn triggers the action of our share component.

You probably want to hide the share button for non-native apps, as they have to use the browser’s share functionality and the button won’t have any functionality when clicked (as it only triggers the bridge message). You can do this by checking if view is rendered for a native app:

<% if hotwire_native_app? %>
  <div data-controller="bridge--share">
    <button data-action="bridge--share#open">Share</button>
  </div>
<% end %>

You can also add css classes to hide functionality for non-native apps. This has the added benefit of having less code in your views, and means there is only one version of the page, which makes caching a lot simpler. This article by Yaroslav Shmarov explains how to do this with Tailwind CSS.

If anyone has written the Android variant of this component, please let me know! I didn’t dive into Android development yet, so haven’t written it yet myself.