iOSのAction Extensionを使って共有から画像を受け取れるようにする

2021/05/08に公開

弊アプリで共有機能から画像を受け取れるようにしたときのメモです。Share Extensionが有名ですが今回はActionを利用しました、赤枠で囲んだリストの項目がActionです。

Shareはその名の通りSNSへの共有やアップロードを想定しているようです、それに対してActionはデータやコンテンツの処理を意図しているようです。リファレンスはこちらです。
https://developer.apple.com/jp/app-extensions/

https://developer.apple.com/library/archive/documentation/General/Conceptual/ExtensibilityPG/Action.html#//apple_ref/doc/uid/TP40014214-CH13-SW1

Action Extensionについて簡単にまとめた記事はこちら
https://zenn.dev/yorifuji/articles/ios-action-extension

本記事で使用した環境

  • Xcode 12.5
  • iOS 14.5

Actionの実装手順

ターゲットの追加

Xcode上でターゲットの追加からActionを追加します。

共有するコンテンツの種類の宣言

追加したTargetのInfo.plistを開いてNSExtensionActivationRuleを編集します。今回は画像を1枚だけ受け取りたかったので以下のように修正しました。

                <key>NSExtensionAttributes</key>
                <dict>
                        <key>NSExtensionActivationRule</key>
-                       <string>TRUEPREDICATE</string>
+                       <dict>
+                               <key>NSExtensionActivationSupportsImageWithMaxCount</key>
+                               <integer>1</integer>
+                       </dict>
                        <key>NSExtensionServiceAllowsFinderPreviewItem</key>
                        <true/>
                        <key>NSExtensionServiceAllowsTouchBarItem</key>

参考までにNSExtensionActivationRuleTRUEPREDICATEの状態でAppStoreConnectにアップロードしたらエラーで弾かれました。

共有された画像の処理

ExtensionのViewControllerに画像を受け取るコードが含まれています。NSItemProvidercompletionHandlerに画像データが含まれているので必要に応じて改変します。写真アプリから共有したときはNSURL形式でパスが渡ってきました、iOSのスクリーンショットの画面から共有したときはURLではなくUIImageで渡ってきました。

provider.loadItem(forTypeIdentifier: kUTTypeImage as String, options: nil, completionHandler: { (data, error) in
    guard let data = data else { return }
    if type(of: data) == NSURL.self {
        // NSURLの処理
    } else if type(of: data) == UIImage.self {
        // UIImageの処理
    } else {
        // Unknown
    }
}

私のアプリでは本体アプリで画像を処理したかったのでFileManagerを使ってローカルディレクトリにファイルとして保存するようにしました。Extensionと本体アプリの両方でファイルの読み書きをするためにApp Groupsを有効にしました。

ファイルを保存するコードです、App Groupsの名前をforSecurityApplicationGroupIdentifierで指定しています。

func copyFileToAppGroup(_ atURL: URL) -> String? {
    let fileName = "yomitori-shared-image-\(UUID().uuidString).\(atURL.pathExtension)"
    if let shareURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.yomitori-ocr-ios") {
        let toURL = shareURL.appendingPathComponent(fileName)
        do {
            try FileManager.default.copyItem(at: atURL, to: toURL)
            return fileName
        } catch {
            return nil
        }
    }
    return nil
}

func saveUIImageToAppGroup(_ image: UIImage) -> String? {
    if let shareURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.example.yomitori-ocr-ios") {
        let fileName = "yomitori-shared-image-\(UUID().uuidString).jpg"
        if let data = image.jpegData(compressionQuality: 1.0) {
            do {
                try data.write(to: shareURL.appendingPathComponent(fileName))
                return fileName
            } catch {
                return nil
            }
        }
    }
    return nil
}

デバッグ

Extensionを選択して実行するとアプリを選択する画面が表示されます。

写真アプリから共有を呼び出すとリストの一番下に項目が表示されています、選択するとExtensionが実行されます。実行中はブレークポイントも使用できます。

リストのテキストの変更

リストに表示されるテキストを変更するにはExtensionのCFBundleDisplayNameを変更します。ローカライズの方法は調べてませんがおそらく通常のアプリと同じ方法で良いのではと思います。

        <key>CFBundleDevelopmentRegion</key>
        <string>$(DEVELOPMENT_LANGUAGE)</string>
        <key>CFBundleDisplayName</key>
-       <string>ActionExtension</string>
+       <string>追加したActionです</string>

リストのアイコンの変更

以下の記事を参考にExtension側のMedia.xcassetsにアイコンを追加しました。なおアイコンの画像は透過PNGを指定する必要があります。透過していない領域がアイコンとして表示されます。

https://stackoverflow.com/questions/39001813/how-do-you-provide-an-icon-for-an-action-extension

正常にセットされるとアイコンが表示されます。

Extensionから本体アプリを呼び出す

Extensionから本体アプリを呼び出して処理を引き継ぐにはURL Schemeを使ってアプリを呼び出すのが定番のようです。openURLをExtensionから利用する方法は以下の記事を参考にしました。

https://qiita.com/ensan_hcl/items/15ea4379bb184cd6f026

@objc func openURL(_ url: URL) {}  // #selector(openURL(_:))はこの関数がないと作れない
func openUrl(url: URL?) {
    let selector = #selector(openURL(_:))
    var responder = (self as UIResponder).next
    while let r = responder, !r.responds(to: selector) {
        responder = r.next
    }
    _ = responder?.perform(selector, with: url)
}
self.openUrl(url: URL(string: "com.example.yomitori-ocr-ios://...")!)

本体アプリ側の処理

URL Schemeを使って呼び出された本体アプリ側では

  • ローカルディレクトリから画像ファイルを読み込んでアプリで処理を引き継ぐ
  • 不要になったファイルを削除

という流れになります。

簡単ですがAction Extensionで画像を受け取る方法でした。

参考資料

http://harumi.sakura.ne.jp/wordpress/2019/07/20/share-extensionで画像を保存する/
https://blog.ch3cooh.jp/entry/20170510/1494378000
https://dev.classmethod.jp/references/ios-8-action-extension/
https://developer.apple.com/jp/app-extensions/
https://qiita.com/KosukeQiita/items/994693da551a7101cc9c
https://qiita.com/justin999/items/fab0e12e0ae269a85a95
https://qiita.com/motokiee/items/1f9147e6eb18f51937af
https://qiita.com/rizumi/items/f25768f9bcb1fc1c45ab
https://qiita.com/shiz/items/d2ba8efa7318c7e70a5a
https://qiita.com/suzuhiroruri@github/items/d3fb996bc5e993fd1a3a
https://stackoverflow.com/questions/53896372/how-to-fetch-screenshot-in-ios-share-extension
https://starhoshi.hatenablog.com/entry/2019/12/30/175052
https://teratail.com/questions/60727

Discussion