iOSのアプリケーション間でテキストを受け渡したい場合、以前はURL Scheme、クリップボード、OpenInぐらいしか方法がなかった。
iOS8からは App Extension という新たなアプリケーション連携の方法が提供されており、Androidのintentのように、任意のアプリケーション間でテキストやイメージが受け渡せるようになった。
iOS8の登場からもう3年も経っているが、遅ればせながらEBPocket Pro for iOSにもApp Extensionを実装してみた。
App Extensionには種類があるが、EBPocketではShare Extensionに対応した。
実装の方法
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のバージョン番号が食い違っていると提出時にワーニングが出るが、これは無視していいらしい。