iOS8のApp Extensionsをつくってみる（Share 実装編）


# App Extensions とは
![app-extensions-icon.png](https://qiita-image-store.s3.amazonaws.com/0/12896/74a40213-9444-4c0a-2bc1-a8334cd37b25.png "app-extensions-icon.png")


App ExtensionsはiOS8から新しく追加されたアプリ連携の仕組みです。

iOS7までは起動中のアプリから別のアプリを立ち上げるには、
URLスキームを使う以外に手段はありませんでした。

iOS8からはApp Extensionsを利用することで、
写真加工やコンテンツのシェアを別アプリに任せたり、アプリ間でのドキュメントの共有、
その他この仕組みを使って通知センターにウィジェットを配置したり、
カスタムキーボードの作成も可能になります。

# Share Extension をつくってみる

今回はShare Extensionについて簡単に説明をしていきます。

Share Extensionはコンテンツをシンプルに共有するのに適しているApp Extensionです。
例えば、Safariや写真アプリ等Apple標準アプリにも、
共有ボタンを押すと対応したアプリが出てくるようになります。
![share.jpg](https://qiita-image-store.s3.amazonaws.com/0/12896/f5a8259c-eff7-67dd-e500-5bb19beb9e74.jpeg "share.jpg")


## 1. ターゲットの作成

App Extensionは既存のアプリにターゲットを追加してやることで簡単に実装できます。
Xcode の [File] &gt; [New] &gt; [Target] から、[Application Extension] &gt; [Share Extension] を選択します。

![add_new_target_2x.png](https://qiita-image-store.s3.amazonaws.com/0/12896/5c23b8bb-a9bd-45ae-fe65-2862a0ed11f6.png "add_new_target_2x.png")
※ App Extension Programming Guide の 図3-1より引用

いつものように自動的にテンプレートが作成されます。
SLComposeServiceViewControllerを継承したViewControllerが1つ生成されるので、
基本的にはこのクラスをベースにカスタマイズしていきます。

アプリをビルドしてシミュレーターや実機で動かすと、
何も機能はありませんが、共有ボタンを押すと追加されていることが確認できると思います。

デフォルトでは共有ボタンからTwitterやfacebookに共有するときに表示されるViewと同じ様なものが用意されており、必要に応じてカスタマイズも可能なようです。

## 2. 共有できるコンテンツを制限する

デフォルトではすべてのコンテンツを共有できるようになっています。
plistで共有可能なコンテンツの種類を制限することができます。

NSExtensionActivationRuleでテキスト、画像、動画、ファイル、Web URL、Webページの6種類について指定可能で、
それぞれいくつまで共有できるか数を指定できます。
対応させない場合は、0を指定するか、keyそのものを消して下さい。

参考：[Information Property List Key Reference: App Extension Keys](https://developer.apple.com/library/prerelease/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/SystemExtensionKeys.html "Information Property List Key Reference: App Extension Keys")

```xml
<key>NSExtensionAttributes</key>
        <dict>
            <key>NSExtensionActivationRule</key>
                <dict>
                    <key>NSExtensionActivationSupportsTextWithMaxCount</key>
                        <integer>1</integer>
                    <key>NSExtensionActivationSupportsImageWithMaxCount</key>
                        <integer>10</integer>
                    <key>NSExtensionActivationSupportsMovieWithMaxCount</key>
                        <integer>1</integer>  
                    <key>NSExtensionActivationSupportsFileWithMaxCount</key>
                        <integer>0</integer>
                    <key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
                        <integer>1</integer>
                    <key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
                        <integer>0</integer>
                </dict>
        </dict>
```

## 3. 投稿の可否を判別する

例えばTwitterで共有するなら140文字以内に収めなければならない、という制約があります。
このように共有先に投稿できるコンテンツかどうか判別する必要があるケースの方が多いと思います。

投稿の可否はisContentValidメソッドで判定します。
このメソッドはユーザーがテキストを編集するたびに呼ばれるメソッドなので、
例えば文字数をこのメソッド内で参照して、YES/NOを返してやります。
（valideteContentメソッドを呼び任意のタイミングで実行することも可能です）

また、入力できる残り文字数や、現在の文字数は
charactersRemainingに値を入れることで、簡単に表示することができます。

```objc
- (BOOL)isContentValid {
    // 現在の文字数を表示する
    self.charactersRemaining = @(self.contentText.length);
   
    // 1文字以上入力されている場合のみ投稿できるようにする
    if (self.contentText.length > 0) {
        return YES;
    }

    return NO;
}
```

## 4. 投稿ボタンが押された時の処理

投稿ボタンが押されると、didSelectPostメソッドが呼ばれます。
例えば、入力されたテキストとURLを何らかのサービスにシェアする例を紹介します。

注意すべき点はNSItemProviderです。
NSItemProviderは共有するアイテムの種類にuniform type identifier (UTI)を利用しています。
例えば、URLであれば、kUTTypeURLという予め定義されている値を利用して取り出します。
UTIにあまり馴染みのない方は以下のページを参考にしてみて下さい。

* [Cocoa Core Competencies: Uniform Type Identifier](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/DevPedia-CocoaCore/UniformTypeIdentifier.html "Cocoa Core Competencies: Uniform Type Identifier")
* [UTType Reference](https://developer.apple.com/library/prerelease/ios/documentation/MobileCoreServices/Reference/UTTypeRef/ "UTType Reference")
* [Uniform Type Identifierの概要](http://potting.syuriken.jp/potting_conv/understanding_utis_J/chapter4.html "Uniform Type Identifierの概要")

なお、以下のサンプルコードでは省略してますが、なんらかのエラーで投稿に失敗した場合には、
cancelRequestWithErrorメソッドを用いて、ホストアプリにエラーを知らせるのが礼儀正しいようです。
その際、userInfoにはNSExtensionItemsAndErrorsKeyをKeyに、valueにNSExtensionItemオブジェクトと関連するNSErrorインスタンスを含む辞書をセットしたものを返すべきとのことです。（たぶん）
 参考：[cancelRequestWithError - NSExtensionContext Class Reference](https://developer.apple.com/library/prerelease/ios/documentation/Foundation/Reference/NSExtensionContext_Class/#//apple_ref/occ/instm/NSExtensionContext/cancelRequestWithError:" title="cancelRequestWithError "cancelRequestWithError - NSExtensionContext Class Reference")


```objc
- (void)didSelectPost {
    // 今回は共有するコンテンツがURL1つに制限していると仮定する

    // 共有するコンテンツを取り出す
    NSExtensionItem *inputItem = self.extensionContext.inputItems.firstObject;
    NSItemProvider *urlItemProvider = inputItem.attachments.firstObject;

    // URLを取り出す
    if ([urlItemProvider hasItemConformingToTypeIdentifier:(__bridge NSString *)kUTTypeURL]) {
        [urlItemProvider loadItemForTypeIdentifier:(__bridge NSString *)kUTTypeURL
                                           options:nil
                                 completionHandler:^(NSURL *url, NSError *error) {
                                     // kUTTypeURLの場合itemはNSURLクラスで渡される
                                     if (!error) {
                                         
                                         // ここでなんらかのサービスに投稿する処理をする

                                         // 投稿に成功したら、必要に応じて実際に投稿したアイテムをこのExtenstionを呼び出したホストアプリに伝える
                                         NSExtensionItem *outputItem = [inputItem copy];
                                         outputItem.attributedContentText = [[NSAttributedString alloc] initWithString:self.contentText attributes:nil];
 
                                         NSArray *outputItems = @[outputItem];
                                         [self.extensionContext completeRequestReturningItems:outputItems expirationHandler:nil completion:nil];
                                     }
    }
}
```

## 5. アカウントの選択等をできるようにする

iOS内臓のTwitter共有の場合、アカウントや位置情報を選択できるカラムが表示されています。
あのカラムを表示するにはconfigurationItemsメソッドを実装します。

下記のサンプルコードでは省略しましたが、WWDC 2014の「Creating Extensions for iOS and OS X, Part 1」セッションでは、
configurationItem.tapHandler内で生成するViewControllerのdelegateにselfをセットして、遷移先で任意の項目が選択された時に、delegateメソッドを呼び、その中でpopConfigurationViewControllerメソッドを呼んでいました。

参考：[Creating Extensions for iOS and OS X, Part 1](https://developer.apple.com/videos/wwdc/2014/#205-video "Creating Extensions for iOS and OS X, Part 1")

```objc
- (NSArray *)configurationItems {

    SLComposeSheetConfigurationItem *configurationItem = [[SLComposeSheetConfigurationItem alloc] init];
   
    // カラムの左側に表示されるタイトル   
    configurationItem.title = @"アカウント";

    // 右側に表示されるデフォルト値（現在選択されている値）
    configurationItem.value = @"monoqlo";

    // アカウント選択等をさせる場合、SLComposeSheetConfigurationItemのtapHandlerに
    // ViewControllerをつくり、そして表示させるブロックを渡す
    // デフォルトではUINavigationControllerで画面遷移して表示される
    configurationItem.tapHandler = ^(void){
        // 適当なViewControllerを渡す
        UIViewController *viewController = [[UIViewController alloc] init];
        [self pushConfigurationViewController:viewController];
    };

    return @[configurationItem];
}
```

## 6. コンテナーアプリとのデータ共有

コンテナーアプリとApp Extension間でデータのやりとりには、
Keychain の他に App Group という新しい仕組みを使うことができます。

group.com.company.appname のようなグループIDを作成し、
コンテナーアプリとApp Extensionの両方に同じIDを設定することで、
そのグループ間のみで、NSUserDefaults、CoreData、ファイルの共有が可能になります。

例えばNSUserDefaultsの場合、initWithSuiteNameというイニシャライザを使い、
先ほどのグループIDを指定することで利用が可能になります。

```objc
// 保存
NSString *username = @"monoqlo";

NSUserDefaults *sharedUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.company.appname"];
[sharedUserDefaults setObject:username forKey:@"username"];
```

```objc
// 取り出し
NSUserDefaults *sharedUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.com.company.appname"];
NSString *username = [sharedUserDefaults stringForKey:@"username"];
```

## 7. デバッグ方法

Xcode6についてはNDAに触れるため、細かく説明できませんが、
App Extensionはコンテナーアプリとはまったく別のプロセスで実行されるため、
単純にアプリを実行しただけではブレークポイントを設置しても止まってくれませんし、
NSLogの出力もXcode上に表示されません。

他にもやり方があるのかもしれませんが、
今回はNSLogを仕込み、シミュレーターや実機のシステムログに出力された結果を元にデバッグをしました。

シミュレーターの場合、
Debug > Open System Log... を選択することで表示させることができます。

# 参考資料

* [App Extension Programming Guide: App Extensions Increase Your Impact](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/ "App Extension Programming Guide: App Extensions Increase Your Impact")
* [App Extension Programming Guide: Share](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/ExtensibilityPG/ShareSheet.html "App Extension Programming Guide: Share")
* [App extension for iOS](http://www.slideshare.net/toyship/app-extension-for-ios "App extension for iOS")