iOSのAction Extensionを使って共有から画像を受け取れるようにする
弊アプリで共有機能から画像を受け取れるようにしたときのメモです。Share Extension
が有名ですが今回はAction
を利用しました、赤枠で囲んだリストの項目がActionです。
Share
はその名の通りSNSへの共有やアップロードを想定しているようです、それに対してAction
はデータやコンテンツの処理を意図しているようです。リファレンスはこちらです。
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>
参考までにNSExtensionActivationRule
がTRUEPREDICATE
の状態でAppStoreConnectにアップロードしたらエラーで弾かれました。
共有された画像の処理
ExtensionのViewControllerに画像を受け取るコードが含まれています。NSItemProvider
のcompletionHandler
に画像データが含まれているので必要に応じて改変します。写真アプリから共有したときは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を指定する必要があります。透過していない領域がアイコンとして表示されます。
正常にセットされるとアイコンが表示されます。
Extensionから本体アプリを呼び出す
Extensionから本体アプリを呼び出して処理を引き継ぐにはURL Scheme
を使ってアプリを呼び出すのが定番のようです。openURL
をExtensionから利用する方法は以下の記事を参考にしました。
@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で画像を受け取る方法でした。
参考資料
Discussion