[iOS 8] App Extension #6 – Embedded Framework と App Group を利用して Action Extension を実装する [後編]
Embedded Framework と App Group を利用して Action Extension を実装する [前編]の続きです。ソースコードは GitHub に公開してあるので参考にして下さい。
Embedded Framework の実装
Embedded Framework に containing app と Extension で利用するイメージの永続化モジュールを実装します。このクラスには、イメージを保存するsaveLatestImage:error: メソッドと、イメージを取り出す loadLatestImage メソッドを定義します。
ImageService.h
#import <Foundation/Foundation.h> #import <UIKit/UIKit.h> @interface ImageService : NSObject - (BOOL)saveLatestImage:(UIImage *)image error:(NSError * __autoreleasing *)error; - (UIImage *)loadLatestImage; @end
イメージの保存
イメージ保存処理のコードは下記の通りです。
ImageService.m
- (BOOL)saveLatestImage:(UIImage *)image error:(NSError *__autoreleasing *)error { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *destURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:AppGroupIdentifier]; NSDateFormatter *dateFormatter = [NSDateFormatter new]; dateFormatter.dateFormat = @"yyyyMMddHHmmss"; NSString *dateString = [dateFormatter stringFromDate:[NSDate new]]; NSString *filePath = [dateString stringByAppendingPathExtension:@"png"]; destURL = [destURL URLByAppendingPathComponent:filePath]; NSData *data = UIImagePNGRepresentation(image); BOOL succeeded = [data writeToURL:destURL atomically:YES]; if (!succeeded) { *error = [NSError errorWithDomain:NSCocoaErrorDomain code:NSFileWriteUnknownError userInfo:nil]; return NO; } NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroupIdentifier]; [defaults setObject:filePath forKey:UserDefaultsLastestImagePathKey]; [defaults synchronize]; return YES; }
ローカルストレージの共有領域へのファイルの保存
まず、NSFileManager の containerURLForSecurityApplicationGroupIdentifier: メソッドで shared container によるローカルストレージの共有領域のパスを取得します。
ImageService.m
NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *destURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:AppGroupIdentifier];
メソッドの引数には、先程設定した App Group の identifier を渡します。
ActionExtensionNoUIConstants.h
static NSString * const AppGroupIdentifier = @"group.jp.classmethod.ActionExtensionNoUISample";
パスを取得したら、共有領域のルート直下に日時をファイル名としてイメージを保存します。
UserDefaults の共有領域へのデータの書き込み
さらに、最新のイメージの保存先についての情報を containing app から取得できるよう、共有領域のルートからの相対パスを shared container による UserDefaults の共有領域に保存しています。
ImageService.m
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroupIdentifier]; [defaults setObject:filePath forKey:UserDefaultsLastestImagePathKey]; [defaults synchronize];
UserDefaults の共有領域には、initWithSuiteName: イニシャライザに App Group の identifier を渡して NSUserDefaults をインスタンス化するとアクセスできるようになります。
イメージの取り出し
イメージ取り出し処理のコードは下記の通りです。
ImageService.m
- (UIImage *)loadLatestImage { UIImage *image = nil; NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:AppGroupIdentifier]; NSString *relativePath = [defaults objectForKey:UserDefaultsLastestImagePathKey]; if (relativePath) { NSFileManager *fileManager = [NSFileManager defaultManager]; NSURL *containerURL = [fileManager containerURLForSecurityApplicationGroupIdentifier:AppGroupIdentifier]; NSString *filePath = [containerURL.path stringByAppendingPathComponent:relativePath]; image = [UIImage imageWithContentsOfFile:filePath]; } return image; }
shared container による UserDefaults の共有領域からイメージ保存先の相対パスを取得します。それを元に、shared container によるローカルストレージの共有領域からイメージを読み込みます。
これで共通モジュールの実装は完了です。あとは、作成したクラスを Embedded Framework 外から利用できるように公開の設定をして下さい。 設定については、こちらの記事を参照して下さい。
Extension の実装
共通モジュールができたので、続いて host app から受け取ったイメージをローカルストレージに保存する Extension を実装します。
NSExtensionRequestHandling プロトコルの適合
先程、Extension のターゲットを作成した際に ActionRequestHandler というクラスが生成されています。このクラスは Info.plist で NSExtensionPrincipalClass に指定されており、host app からの呼び出しをハンドリングします。生成されたヘッダファイルは下記のように NSExtensionRequestHandling プロトコルに適合しています。
ActionRequestHandler.h
#import <UIKit/UIKit.h> @interface ActionRequestHandler : NSObject <NSExtensionRequestHandling> @end
host app からの呼び出しに対するハンドリング
ActionRequestHandler の実装は下記の通りです。
ActionRequestHandler.m
#import "ActionRequestHandler.h" #import <MobileCoreServices/MobileCoreServices.h> @import ActionExtensionNoUIEmbeddedLib; @interface ActionRequestHandler () @property (nonatomic, strong) NSExtensionContext *extensionContext; @end @implementation ActionRequestHandler - (void)beginRequestWithExtensionContext:(NSExtensionContext *)context { self.extensionContext = context; NSExtensionItem *item = self.extensionContext.inputItems.firstObject; NSItemProvider *itemProvider = item.attachments.firstObject; if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { __weak typeof(self) weakSelf = self; [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage options:nil completionHandler:^(id<NSSecureCoding> item, NSError *error) { if (error) { [weakSelf.extensionContext cancelRequestWithError:error]; return; } if (![(NSObject *)item isKindOfClass:[UIImage class]]) { NSError *unavailableError = [NSError errorWithDomain:NSItemProviderErrorDomain code:NSItemProviderUnexpectedValueClassError userInfo:nil]; [weakSelf.extensionContext cancelRequestWithError:unavailableError]; return; } UIImage *image = (UIImage *)item; NSError *serviceError = nil; ImageService *service = [ImageService new]; [service saveLatestImage:image error:&serviceError]; if (serviceError) { [weakSelf.extensionContext cancelRequestWithError:serviceError]; return; } [weakSelf.extensionContext completeRequestReturningItems:nil completionHandler:nil]; }]; } else { NSError *unavailableError = [NSError errorWithDomain:NSItemProviderErrorDomain code:NSItemProviderItemUnavailableError userInfo:nil]; [self.extensionContext cancelRequestWithError:unavailableError]; } } @end
beginRequestWithExtensionContext: メソッドで host app からの呼び出しをハンドリングしています。
host app から渡されたアイテムの確認とロード
host app から渡されたアイテムの確認とロードに関しては、通常の Action Extension と同様です。
__weak typeof(self) weakSelf = self; if ([itemProvider hasItemConformingToTypeIdentifier:(NSString *)kUTTypeImage]) { [itemProvider loadItemForTypeIdentifier:(NSString *)kUTTypeImage 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]; }
イメージの保存処理の実行と host app への処理完了の通知
下記コードは、イメージの保存処理の実行と host app への処理完了の通知を行う部分を抜粋したものです。
UIImage *image = (UIImage *)item; NSError *serviceError = nil; ImageService *service = [ImageService new]; [service saveLatestImage:image error:&serviceError]; if (serviceError) { [weakSelf.extensionContext cancelRequestWithError:serviceError]; return; } [weakSelf.extensionContext completeRequestReturningItems:nil completionHandler:nil];
Embedded Framework に実装した ImageService を利用してイメージを shared container による共有領域に保存しています。また、処理完了後、NSExtensionContext の completeRequestReturningItems:completionHandler: を呼び出して host app に処理完了を通知しています。
containing app の実装
containing app は、Extension によって保存されたイメージのうち、最新のものを画面に表示するだけの簡単なものです。
UI の作成
Storyboard は下図のように、UIImageView を画面いっぱいに配置し、Content Mode を Aspect Fit にします。AutoLayout の制約を定義し忘れると実行時にイメージが画面からはみ出してしまうので注意して下さい。
ViewController の実装
ViewController の実装もシンプルです。Embedded Framework に実装した ImageService を利用して、画面の初期化時に Extension によって保存された最新のイメージをロードし、UIImageView に渡しています。
ViewController.m
#import "ViewController.h" @import ActionExtensionNoUIEmbeddedLib; @interface ViewController () @property (weak, nonatomic) IBOutlet UIImageView *imageView; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; ImageService *service = [ImageService new]; self.imageView.image = [service loadLatestImage]; } @end
host app の実装
最後に、Extension を利用する host app を作成します。host app では、カメラロールからイメージを取得し、それを Activity View から Extension に渡せるようにします。
host app 用に Xcode でプロジェクトを作成してください。
UI の作成
Storyboard は下図のような構成です。ViewController に UIImageView を配置し、Navigation Bar 部分にカメラボタンとアクションボタンを配置しています。
カメラロールからイメージを取得
カメラロールからイメージを取得する部分のコードは下記の通りです。
- (IBAction)cameraButtonDidTouch:(id)sender { UIImagePickerController *controller = [UIImagePickerController new]; controller.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; controller.delegate = self; [self presentViewController:controller animated:YES completion:nil]; } - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { UIImage *image = [info objectForKey:UIImagePickerControllerOriginalImage]; self.imageView.image = image; [self dismissViewControllerAnimated:YES completion:nil]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [self dismissViewControllerAnimated:YES completion:nil]; }
カメラボタンをタッチされた際に、UIImagePickerController を表示してカメラロール内にあるイメージを選択できるようにします。イメージが選択されたら、UIImagePickerControllerDelegate の imagePickerController:difFinishPickingmedaWithInfo: が呼び出されるので、ここでイメージを取り出して UIImageView にセットしています。
Extension の呼び出し
Extension の呼び出しを行う部分のコードは下記の通りです。
- (IBAction)actionButtonDidTouch:(id)sender { UIActivityViewController *viewController = [[UIActivityViewController alloc] initWithActivityItems:@[self.imageView.image] applicationActivities:nil]; [viewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { // ... }]; [self presentViewController:viewController animated:YES completion:nil]; }
アクションボタンがタッチされた際に UIActivityViewController を表示します。initWithActivityItems:applicationActivities: の一番目のパラメータに Extension に引き渡したいデータを渡します。今回は、カメラロールから取得したイメージを渡しています。
また、Extension の処理完了後のコールバックとして、setCompletionWithItemsHandler: メソッドに Blocks をセットしています。
Extension 呼び出しのコールバック
下記コードは、Extension の処理が完了もしくはキャンセルされた際に呼び出されるコールバック部分の抜粋です。
[viewController setCompletionWithItemsHandler:^(NSString *activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { NSString *message; if (activityError) { message = activityError.localizedDescription; } else { message = @"イメージを保存しました"; } UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *action = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { [self dismissViewControllerAnimated:YES completion:nil]; }]; [alertController addAction:action]; [self presentViewController:alertController animated:YES completion:nil]; }];
処理結果に応じてメッセージを作成し、アラートを表示しています。
動作確認
さて、これで containing app, Extension, host app の全ての実装が完了しましたので、動作を確認してみたいと思います。動作確認をする前に、必ず Extension を Xcode からデバッグ起動して、端末もしくはシミュレータにインストールしておいて下さい。
まずは、host app を起動してカメラボタンをタップします。
カメラロールへのアクセス許可を確認されますので、OK をタップしてください。
Picker View が表示されたら、適当なイメージを選択します。
host app の画面に選択したイメージが表示されます。
次に、アクションボタンをタップして Activity View を表示します。
Activity View の下側のリストに Extension が候補として表示されています。
Extension を選択して下さい。少し待つと、Extension の処理が完了して host app に通知され、アラートが表示されます。
containing app を起動すると、先程 Extension に渡したイメージが画面に表示されているのが確認できると思います。
まとめ
Embedded Framework と App Group は最初こそ設定が面倒に感じますが、一度手順を把握すれば利用するのは比較的簡単です。App Extension を開発する際には積極的に活用していきましょう。