[iOS 8] App Extension #4 – Action Extension

Action Extension

Action Extension は iOS 8 より利用できるようになった App Extension が提供する機能の一部です。これは、アプリが提供する機能を他のアプリから利用できるようにする Extension で、Extension の中でも割り合い汎用的な機能を備えています。

Action Extension の主な利用用途としては、他のアプリが保持しているデータを独自の形式で表示したり、他のアプリが保持しているデータに対して、ユーザーが編集を行うための独自の UI を提供するといったことが挙げられます。

サンプル Extension の作成

今回は、host app から Web ページの URL を受け取って WebView でページを表示する Extension のサンプルを作成したいと思います。ソースコードは GitHub に公開してあるので参考にして下さい。

containing app, Extension プロジェクトの作成

Xcode から Single View Application でプロジェクトを作成します。

プロジェクトに Extension を追加するためには、Extension の Target を追加する必要があります。 Project navigator で作成したプロジェクトを選択すると、Editor ペインにプロジェクトの設定が表示されますので、その左下にある + ボタンをクリックします。

ios-actionextension_001

ターゲットのテンプレートを選択するダイアログが表示されますので、左側のカテゴリで iOS > Application Extension 選択します。選択すると右側に Extension のテンプレート一覧が表示されますので、Action Extension を選択して Next をクリックしてください。

ios-actionextension_002

続いて、オプションを入力する画面が表示されます。Product Name に Extension の名前を入力します。Action Type は Presents User Interface に設定します。Finish をクリックすると、Extension のターゲットが作成されます。

ios-actionextension_003

作成したターゲットのアクティベーションダイアログが表示された場合には、Activate を選択します。

ios-actionextension_004

以上で、Action Extension のターゲット作成が完了しました。

ios-actionextension_005

プロジェクトの構成

Extension のターゲット作成後に Project navigator を確認すると、ターゲットのフォルダと下記のファイルが作成されています。

  • ActionViewController.h
  • ActionViewController.m
  • MainInterface.storyboard
  • Info.plist

ios-actionextension_006

MainInterface.storyboard

host app から Extension が呼び出された際に表示するビューを定義した Storyboard です。

ActionViewController.h, .m

MainInterface.storyboard で InitialViewController として設定されている ViewController です。Extension が呼び出された際に MainInterface.storyboard がロードされ、同時に InitialViewController であるこの ViewController もロードされます。ロード後には通常通り viewDidLoad などが呼び出されるため、ここで初期化処理などを行います。

Info.plist

Extension の設定が定義されている Info.plist です。NSExtension キーの値として、Extension 固有の設定が定義されています。

ios-actionextension_007

NSExtensionActivationRule

NSExtensionActivationRule は、主に host app から渡されたデータがどのようなタイプのデータであれば Extension が受け入れるかということについて定義します。有効なキーは下記の8つです。

  • NSExtensionActivationSupportsAttachmentsWithMaxCount
  • NSExtensionActivationSupportsAttachmentsWithMinCount
  • NSExtensionActivationSupportsFileWithMaxCount
  • NSExtensionActivationSupportsImageWithMaxCount
  • NSExtensionActivationSupportsMovieWithMaxCount
  • NSExtensionActivationSupportsText
  • NSExtensionActivationSupportsWebURLWithMaxCount
  • NSExtensionActivationSupportsWebPageWithMaxCount

タイプにはファイルやイメージなどが定義されています。個々の値の詳細については Apple の公式ドキュメントの NSExtensionActivationRule を参照して下さい。

ここで受け入れることを宣言されたタイプのデータが host app で生成した UIActivityViewController にアタッチされている場合に、Activity View 上で Extension が候補として表示されることになります。

また、テキストを除いた項目に WithMaxCount という名前がついていることから想像できる通り、Extension が受け付けることができるデータの最大数をタイプ別に定義します。これを超える数のデータが渡された場合には、この Extension は Activity View に Activity の候補として表示されません。また、これらの数を 0 に設定した場合にも、Activity View に表示されなくなります。

この値は、初期状態では TRUEPREDICATE になっています。これは開発時専用のオプションで、全てのタイプのデータを受け入れます。ただし、この設定のまま App Store に申請をするとリジェクトされてしまいます。必ず Extension が対応しているタイプのデータのみを受け入れるよう設定を変更しましょう。

今回作成する Extension は URL を受け取って処理を行うため、NSExtensionActivationSupportsWebURLWithMaxCount を 1 に設定しておきます。

ios-actionextension_008

Extension の実装

host app から呼び出された際に表示するビューを作成します。

UI の作成

Storyboard は、UIWebViewUINavigationBar を配置しているだけの簡単なものです。Navigation Bar には、UIBarButtonItem を配置して、このビューを閉じる処理を呼び出します。

ios-actionextension_009

ViewController の実装

ViewController のコードは下記の通りです。

ActionViewController.m

#import "ActionViewController.h"
#import <MobileCoreServices/MobileCoreServices.h>

@interface ActionViewController () <UIWebViewDelegate>

@property (weak, nonatomic) IBOutlet UIWebView *webView;

@end

@implementation ActionViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSExtensionItem *item = self.extensionContext.inputItems.firstObject;
    NSItemProvider *itemProvider = item.attachments.firstObject;

    if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {

        __weak typeof(self) weakSelf = self;
        [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeURL options:nil completionHandler:^(id<NSSecureCoding> item, NSError *error) {
            if (error) {
                [weakSelf.extensionContext cancelRequestWithError:error];
                return;
            }

            if (![(NSObject *)item isKindOfClass:[NSURL class]]) {
                NSError *unexpectedError = [NSError errorWithDomain:NSItemProviderErrorDomain
                                                               code:NSItemProviderUnexpectedValueClassError
                                                           userInfo:nil];
                [weakSelf.extensionContext cancelRequestWithError:unexpectedError];
                return;
            }

            NSURL *url = (NSURL *)item;

            [weakSelf.webView loadRequest:[NSURLRequest requestWithURL:url]];
        }];
    } else {
        NSError *unavailableError = [NSError errorWithDomain:NSItemProviderErrorDomain
                                                        code:NSItemProviderItemUnavailableError
                                                    userInfo:nil];
        [self.extensionContext cancelRequestWithError:unavailableError];
    }
}


- (IBAction)done {
    [self.extensionContext completeRequestReturningItems:nil
                                       completionHandler:nil];
}

- (void)webViewDidStartLoad:(UIWebView *)webView {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSLog(@"%s", __PRETTY_FUNCTION__);
}

- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
    if (error) {
        NSLog(@"%@", error);
    }
}

@end

NSExtensionContext

UIViewController は NSExtensionContext を格納する extensionContext プロパティを持っています。UI のある Extension が host app から呼び出された場合には、このプロパティに NSExtensionContext のインスタンスが既にセットされた状態になっています。

下記コードでは、ViewController の extensionContext から NSExtensionContext のインスタンスを参照し、NSExtensionItem のインスタンスを取り出しています。

NSExtensionItem *item = self.extensionContext.inputItems.firstObject;

host app から渡されたアイテムの確認とロード

下記コードは、host app から渡されたアイテムの確認とロードを行う部分を抜粋したものです。

if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeURL]) {

    __weak typeof(self) weakSelf = self;
    [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeURL options:nil completionHandler:^(id<NSSecureCoding> item, NSError *error) {
        if (error) {
            [weakSelf.extensionContext cancelRequestWithError:error];
            return;
        }

        // ...
    }];
} else {
    NSError *unavailableError = [NSError errorWithDomain:NSItemProviderErrorDomain
                                                    code:NSItemProviderItemUnavailableError
                                                userInfo:nil];
    [self.extensionContext cancelRequestWithError:unavailableError];
}

まず、取り出した NSItemProvider に対して、hasItemConformingToTypeIdentifier: メソッドで URL を持っているか確認します。URL を持っていなかった場合には、NSExtensionContextcancelRequestWithError: メソッドに NSError を渡して host app に処理の失敗を通知しています。

アタッチされた URL データをロードしたら、WebView に URL をロードさせます。

host app の実装

Extension を利用する host app を実装します。host app は特定の URL を UIActivityViewController にアタッチして Activity View を表示するだけの簡単なものです。

UI の作成

Storyboard は下図のような構成です。UIButton を1つ配置しているだけです。

ios-actionextension_010

ViewController の実装

ボタンがタッチされたら、Activity View を表示します。コードは下記の通りです。

- (IBAction)buttonDidTouch:(id)sender {
    NSURL *url = [NSURL URLWithString:@"https://dev.classmethod.jp/"];
    UIActivityViewController *viewController = [[UIActivityViewController alloc] initWithActivityItems:@[url]
                                                                                 applicationActivities:nil];

    [viewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) {
        if (activityError) {
            NSLog(@"%@", activityError);
            return;
        }
    }];

    [self presentViewController:viewController
                       animated:YES
                     completion:nil];
}

initWithActivityItems:applicationActivities: の一番目のパラメータに Extension に引き渡したいデータを渡します。また、Extension の処理完了後のコールバックとして、setCompletionWithItemsHandler: メソッドに Blocks をセットしています。

動作確認

さて、これで Extension と host app の実装が完了しました。実際に動作を確認してみましょう。

デバッグ起動

Extension を Xcode からデバッグ起動する際には、host app を先にシミュレータや実機にインストールしておく必要があります。host app をデバッグ起動してインストールしておいて下さい。

次に Extension のスキーマを選択してデバッグを実行します。

ios-actionextension_011

すると、Extension を呼び出す host app を選択するダイアログが表示されますので、先程実装した host app を選択して Run をクリックします。

ios-actionextension_012

動作確認

host app が起動しますので、ボタンをタップします。

ios-actionextension_013

Activity View が表示され、Extension が Activity の候補として表示されます。

ios-actionextension_014

下側のリストの右端に More という項目がありますが、これはユーザーが Activity の有効・無効を切り替えるためのリストを表示します。

ios-actionextension_015

More をタップすると Activity のリストが表示され、Extension が有効化されているのが確認できます。ここでスイッチを切り替えて Extension を無効化すると、先程の Activity リストに Extension が表示されなくなります。iOS 8 が Beta だった頃はデフォルトでは Extension は無効になっていましたが、リリース直前にデフォルトで有効になるよう変更されたようです。

ios-actionextension_016

Activity View に戻り Extension をタップすると、host app から渡された URL のページが Extension で提供される WebView に表示されます。

ios-actionextension_017

Done ボタンをタップすると、host app の呼び出し元の画面が再び表示されます。

まとめ

他のアプリからデータを渡してそのデータを表示・編集するという事に関しては、iOS SDK で以前より提供されている UIDocumentInteractionController でも可能です。しかし、Action Extension を利用することによって、データを持っている呼び出し元のアプリからシームレスに様々なアプリの機能を利用することができるようになります。Action Extension の実装自体は特に難しいものでもないので、アプリを企画する際には Action Extension を提供することも視野に入れておくと良さそうですね。