iOS の Share Extensionで Safari からアプリを開く

kyamada,iOSShare ExtensionSafari
2024/9/21追記: iOS18以上でも動作するようにソースコードを修正しました。

概要

小説を聞こう (opens in a new tab)の iOS アプリに Share Extension を導入し、Safari からアプリを開く機能を実装しました。 今回はその実装方法を説明します。

Share extensions と Action extension のどちらを使うか

Safari からアプリを開く機能を実装するには、Share extension と Action extension のどちらかを使えばよさそうです。 Apple のヒューマンインターフェースガイドラインに、使い分けの記載がありました。

Share and action extensions (opens in a new tab)

Share extensions give people a convenient way to share information from the current context with apps, social media accounts, and other services. Action extensions let people initiate content-specific tasks — like adding a bookmark, copying a link, editing an inline image, or displaying selected text in another language — without leaving the current context.

Share extension は、現在のコンテクストからアプリケーション、ソーシャルメディアアカウント、その他のサービスと情報を共有するための便利な方法を提供します。Action extension では、ブックマークの追加、リンクのコピー、インライン画像の編集、選択したテキストの別言語表示など、コンテンツ固有のタスクを、現在のコンテキストを離れることなく開始することができます。

今回は Safari からアプリを開く機能を実装するので、Share extension を使うことにしました。

動作の流れ

  1. Host app(Safari)の共有ボタンをタップ > Containing app(小説を聞こう)をタップ。
  1. Share Extension は Host app の URL を取得し、Custom URL Scheme で Containing app を開く。その時、Host app の URL は Base64 エンコードして Custom URL Scheme のホスト部に指定する(例: listen-to-novels://aHR0cHM6Ly95b21vdS5zeW9zZXR1LmNvbS9yYW5rL2dlbnJldG9wLw==)
  2. Containing app は Custom URL Scheme のホスト部を Base64 デコードして、その URL を WebView で表示する。

Containing app の実装

1. Custom URL Scheme を設定する

Target > Info > URL Types に以下のように入力してください。

2. SwiftUI で onOpenURL イベントをハンドリングする

メインの WindowGroup 直下のビューに onOpenURL 修飾子を指定します。 その中で、Custom URL Scheme のホスト部に指定された URL を開く処理を実装します。

ListenToNovelsApp.swift
@main
class ListenToNovelsApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
                .onOpenURL(perform: { url in
                    // url.hostにURLをbase64エンコードした文字列が入っているので、デコードしてURLを開く
                    if let host = url.host,
                       let data = Data(base64Encoded: host),
                       let urlString = String(data: data, encoding: .utf8) {
                        // URLをWebViewで開く処理
                        self.store.dispatch(MainAction.GoToNovelPage(url: urlString))
                    }
                })
        }
    }

Share Extension の実装

1. Share Extension を Target に追加

File > New > Target... から Share Extension を選択。

Product Name は「ShareExtension」にしました。

2. ShareViewController.swift を書き換える

Host app から渡された URL を取得し、Custom URL Scheme で Containing app を開く処理を実装します。

ShareViewController.swift
import SwiftUI
import UniformTypeIdentifiers
 
class ShareViewController: UIHostingController<ShareView> {
    enum ShareError: Error {
        case cancel
    }
 
    required init?(coder: NSCoder) {
        super.init(coder: coder, rootView: ShareView())
    }
 
    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            _ = await openAppWithUrl()
        }
    }
 
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        extensionContext?.completeRequest(returningItems: nil)
    }
 
    private func openAppWithUrl() async -> Bool {
        guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
              let itemProvider = item.attachments?.first else { return false }
        guard itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) else { return false }
        do {
            let data = try await itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil)
            // URLをbase64エンコードし、Custom URL Schemeのホスト部に指定してopenURLでアプリを開く
            guard let url = data as? NSURL,
                  let base64EncodedUrl = url.absoluteString?.data(using: .utf8)?.base64EncodedString(),
                  let openAppUrl = URL(string: "listen-to-novels://\(base64EncodedUrl)")
            else { return false }
            return await UIApplication.sharedApplication().open(openAppUrl)
        } catch let error {
            print(error)
            return false
        }
    }
}

3. ShareView.swift を作成

このクラスは SwiftUI で UI を構築するためにありますが、今回は UI は使わないので、body に EmptyView()を指定します。

ShareView.swift
import SwiftUI
 
struct ShareView: View {
    var body: some View {
        EmptyView()
        // UIが必要な場合はここに書く
        // https://qiita.com/mume/items/61091237085d9948724c
    }
}

4. UIApplication+.swift を作成

Share extension から openURL メソッドを呼び出すための黒魔術を書きます。

UIApplication+.swift
import UIKit
 
extension UIApplication {
 
    // https://stackoverflow.com/a/36925156/4791194
    public static func sharedApplication() -> UIApplication {
        guard UIApplication.responds(to: Selector(("sharedApplication"))) else {
            fatalError("UIApplication.sharedKeyboardApplication(): `UIApplication` does not respond to selector `sharedApplication`.")
        }
 
        guard let unmanagedSharedApplication = UIApplication.perform(Selector(("sharedApplication"))) else {
            fatalError("UIApplication.sharedKeyboardApplication(): `UIApplication.sharedApplication()` returned `nil`.")
        }
 
        guard let sharedApplication = unmanagedSharedApplication.takeUnretainedValue() as? UIApplication else {
            fatalError("UIApplication.sharedKeyboardApplication(): `UIApplication.sharedApplication()` returned not `UIApplication` instance.")
        }
 
        return sharedApplication
    }
}

5. Info.plist の設定

  1. NSExtensionActivationRule の Type を Dictionary に変更します。
  2. その下に NSExtensionActivationSupportsWebURLWithMaxCount を追加します。

参考

© 品川アプリ.RSS