iOS の Share Extensionで Safari からアプリを開く
概要
小説を聞こう (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 を使うことにしました。
動作の流れ
- Host app(Safari)の共有ボタンをタップ > Containing app(小説を聞こう)をタップ。


- Share Extension は Host app の URL を取得し、Custom URL Scheme で Containing app を開く。その時、Host app の URL は エンコードして Custom URL Scheme のホスト部に指定する(例: listen-to-novels://https%3A%2F%2Fwww.google.com)
- Containing app は Custom URL Scheme のホスト部を デコードして、その URL を WebView で表示する。

Containing app の実装
1. Custom URL Scheme を設定する
Target > Info > URL Types に以下のように入力してください。
- Identifier: $(PRODUCT_BUNDLE_IDENTIFIER)
- URL Scheme: 任意で他のアプリと重複しない文字列
- Icon: None
- Role: Editor
 
2. SwiftUI で onOpenURL イベントをハンドリングする
メインの WindowGroup 直下のビューに onOpenURL 修飾子を指定します。 その中で、Custom URL Scheme のホスト部に指定された URL を開く処理を実装します。
@main
class ListenToNovelsApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
                .onOpenURL(perform: { url in
                    // url.hostにURLをパーセントエンコードした文字列が入っているので、デコードしてURLを開く
                    if let host = url.host,
                       let urlString = host.removingPercentEncoding {
                        // 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 を開く処理を実装します。
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をパーセントエンコードし、Custom URL Schemeのホスト部に指定してopenURLでアプリを開く
            guard let url = data as? NSURL,
                  let encodedUrl = url.absoluteString?.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed),
                  let openAppUrl = URL(string: "listen-to-novels://\(encodedUrl)")
            else { return false }
            return await UIApplication.sharedApplication().open(openAppUrl)
        } catch let error {
            print(error)
            return false
        }
    }
}3. ShareView.swift を作成
このクラスは SwiftUI で UI を構築するためにありますが、今回は UI は使わないので、body に EmptyView()を指定します。
import SwiftUI
 
struct ShareView: View {
    var body: some View {
        EmptyView()
        // UIが必要な場合はここに書く
        // https://qiita.com/mume/items/61091237085d9948724c
    }
}4. UIApplication+.swift を作成
Share extension から openURL メソッドを呼び出すための黒魔術を書きます。
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 の設定
- NSExtensionActivationRule の Type を Dictionary に変更します。
- その下に NSExtensionActivationSupportsWebURLWithMaxCount を追加します。
 
参考
- Android 版の実装はこちら
- Creating an App Extension (opens in a new tab)
- SwiftUI で Share Extension を実装する (opens in a new tab)
- [SwiftUI]Deep Link で値を受け取り画面を出し分けする (opens in a new tab)
- How do you create a Share Extension in iOS8 without a share UI (opens in a new tab)