iOSのアプリケーション間でテキストを受け渡したい場合、以前はURL Scheme、クリップボード、OpenInぐらいしか方法がなかった。
iOS8からは App Extension という新たなアプリケーション連携の方法が提供されており、Androidのintentのように、任意のアプリケーション間でテキストやイメージが受け渡せるようになった。
iOS8の登場からもう3年も経っているが、遅ればせながらEBPocket Pro for iOSにもApp Extensionを実装してみた。
App Extensionには種類があるが、EBPocketではShare Extensionに対応した。
共有の画面例
Safariやメモで文字列を範囲選択し、[共有...]を実行すると、App Extensionに対応したアプリの一覧が表示される。
ここでEBPocketを選択し、Postを押すとEBPoketが起動して受け渡された文字列で検索を実行する。
左上の戻るボタンを押すと呼び出し元のアプリに戻る。
スプリットビューが使用できるiPadでは、SafariとEBPocketを両方開いたままにして、Safariで文字選択した文字列でEBPocketで検索できる。
実装の方法
App Extensionのプログラミングについては、Appleから日本語訳が出ている.
日本語ドキュメント - Apple Developer
App Extensionは単独で配布することはできず、必ず収容アプリと同時に配布しないといけない。今回はEBPocketが収容アプリということになる。
App Exensionと収容アプリとは別のバイナリであり、開発言語が異なっていてもいい。今回は勉強のため、Swift3で書くことにした。
ただし、実行できるiOSのバージョンの条件がiOS7.0以上になったが、現在では困る人はほとんどいないと思う。
Xcodeで 既存のアプリにApp Extensionを追加するのは簡単で、
[File]->[New]->[Target...]を押してApp Extensionの種類からShare Extensionを選択し、Product Nameを入力する。
収容アプリの Bundle Identifire にProduct Nameを連結したものが、App ExtensionのBundle Identifireになる。
収容アプリのBundle Identifire : info.ebstudio.EBPocketPro App ExtensionのBundle Identifire : info.ebstudio.EBPocketPro.shareExtension
ここで次の3つのファイルが生成されるので、これを雛形としてコードを追加していく。
- ShareViewController.swift
- Maininterface.storyboard
- info.plist
info.plistでは:
- Bundle display name にextensionの表示名を設定(この場合EBPocket)
- 共有でテキストのみ選択できるようにNSExtensionActivationRuleを修正する
NSExtension NSExtensionAttributes NSExtensionActivationRule NSExtensionActivationSupportsText Boolean YES
収容アプリと同じアイコンを使用するには、Targetに収容アプリの .xcassets のアイコン名を設定する:
Build Phases Copy Bundle Resources + 収容アプリの .xcassets を追加する Build Settings Asset Catalog App Icon Set Name -> AppIcon (.xcassetsのアイコン名)
ここで一つ困ったことは、Share Extension から収容アプリを起動する方法が URL Scheme しかないのだが、公式の方法である openURL:completionHandler: を実行できるExtensionの種類がToday Extensionだけという問題である。
これは世界中でみんな困っていて、ネットで検索するとさまざまな回避方法が出ている。
最終的に、iOS10でも実行できるSwift3の最終的なソースは次のようになった。
// // ShareViewController.swift // shareExtension // // Created by hishida on 2017/06/14. // // import UIKit import Social class ShareViewController: SLComposeServiceViewController { override func isContentValid() -> Bool { // Do validation of contentText and/or NSExtensionContext attachments here let canPost: Bool = self.contentText.characters.count > 0 if canPost { return true } return false } override func didSelectPost() { // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. let inputItem: NSExtensionItem = self.extensionContext?.inputItems[0] as! NSExtensionItem let itemProvider = inputItem.attachments![0] as! NSItemProvider if (itemProvider.hasItemConformingToTypeIdentifier("public.plain-text")) { itemProvider.loadItem(forTypeIdentifier: "public.plain-text", options: nil, completionHandler: { (item, error) in let URLSCHEME:String = "ebpocket://search?text=" let encodedString:String = self.contentText.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)! let myUrlStr:String = URLSCHEME + encodedString let url = NSURL( string:myUrlStr ) let context = NSExtensionContext() context.open(url! as URL, completionHandler: nil) var responder = self as UIResponder? while (responder != nil){ if responder?.responds(to: Selector("openURL:")) == true{ responder?.perform(Selector("openURL:"), with: url) } responder = responder!.next } }) } // Inform the host that we're done, so it un-blocks its UI. Note: Alternatively you could call super's -didSelectPost, which will similarly complete the extension context. self.extensionContext!.completeRequest(returningItems: , completionHandler: nil) } override func configurationItems() -> [Any]! { // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. return } }
開発作業以上に手間取ったのが、App Storeに提出するための証明書の問題だった。
わかってみるとなんでもないが、App Extensionと本体の収容アプリは別のバイナリのため、Application Identifier、Provisioning profileは全て収容アプリとは別にする必要がある。
- iOS Dev Centerで Extension用の Application IdentifierとProvisioning profileを作成する
- 収容アプリのEntitlements.plistとは別に、App Extension用のEntitlements.plistを作成し、ここでApp ExtensionのApplication Identifierを定義する
ちなみApplication Identifierとは、開発者を表す10桁のprefix + Bundle Identifierのこと:
XXXXXXXXXX.info.ebstudio.EBPocketPro.shareExtension
あと、収容アプリと App Extensionのバージョン番号が食い違っていると提出時にワーニングが出るが、これは無視していいらしい。