hishidaの開発blog

EBシリーズ(EBPocket,EBWin,EBMac,EBStudio),KWIC Finder,xdoc2txt,読書尚友の開発者ブログ

EBPocket for iOS のApp Extension対応について

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では:

  1. Bundle display name にextensionの表示名を設定(この場合EBPocket)
  2. 共有でテキストのみ選択できるように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は全て収容アプリとは別にする必要がある。

  1. iOS Dev Centerで Extension用の Application IdentifierとProvisioning profileを作成する
  2. 収容アプリのEntitlements.plistとは別に、App Extension用のEntitlements.plistを作成し、ここでApp ExtensionのApplication Identifierを定義する

ちなみApplication Identifierとは、開発者を表す10桁のprefix + Bundle Identifierのこと:

 XXXXXXXXXX.info.ebstudio.EBPocketPro.shareExtension 

あと、収容アプリと App Extensionのバージョン番号が食い違っていると提出時にワーニングが出るが、これは無視していいらしい。