Photo Editing Extension とは iOS8で追加された App Extension の一つで、「写真(Photos)」アプリで独自の写真や動画の編集機能を提供することができます。

 そこで今回は「ファミキャプ」に、画像をレトロゲームの画面のようなイメージに加工する機能拡張を追加してみました。

 実際に完成した画面が以下のようになります。

alt text

Photo Editing Extensionのサンプルの実装

 Photo Editing Extension を追加する方法について、サンプルアプリを作成しながら手順を追って説明していきたいと思います。

新規プロジェクトの作成

 Xcodeから新規プロジェクトで Single View Application を選択してプロジェクトを作成します。プロジェクト名は「PhotoEditingSample」としました。

Photo Editing Extensionの作成

 続いて Photo Editing Extension を Target に追加します。以下の画像のように Xcode のメニューから、File > New > Target…を選択します。

alt text

 表示されたダイアログから、iOS > Application Extension > Photo Editing Extensionを選択し、Nextボタンをクリックします。

alt text

 続けて表示されるダイアログの Product Name にプロジェクト名を入力します。ここでは「PhotoEditing」と入力し、Finish ボタンをクリックします。

alt text

 以上の手順を踏むと、プロジェクトツリー上には PhotoEditing で必要なファイルが「PhotoEditing」ディレクトリ内に追加され、TARGETS にも「PhotoEditing」が追加されたのが確認できるかと思います。

alt text

Info.plistの設定

 まずは Info.plist の設定を確認します。プロジェクトツリー上に作成された Extension 用の Info.plist を開いてみると以下のようになっています。

alt text

 PHSupportedMediaTypesには編集可能なコンテンツのタイプを指定します。
コチラのサイトに記載がありますが、Photo Editing Extension で指定できる MediaType は ImageVideo のみになります。

 対応したいコンテンツに応じて plist を更新してください。それ以外の項目に関しては、デフォルトのままで問題ないでしょう。

Storyboard の編集

 Photo Editing Extension での写真の編集画面を作成します。

 写真アプリ側から編集画面が呼び出された際には、デフォルトだと MainInterface.storyboard の画面が起動するため、この Storyboard を編集する必要があります。

 今回は以下の画像のように、単純にImageViewだけを配置した画面を作成してます。

alt text

コードの編集

 Photo Editing Extension では、PhotoEditingViewController クラスのコードを修正していきます。このクラスが写真アプリ側から呼び出される編集画面の ViewController のクラスとなります。

 PhotoEditingViewController を見ると、PHContentEditingControllerというプロトコルに準拠しています。

 この PHContentEditingController プロトコルではいくつかのコールバック用のメソッドが定義されており、Photo Editing Extension ではこのメソッド内に処理を記述していく事で実装を行います。

 PHContentEditingController プロトコルで定義されているメソッドやプロパティは以下のものがあります。

 Photo Editingでは基本的にはこれらのメソッドに対して処理を実装していくことになります。それでは順番に処理を実装していきたいと思います。

canHandleAdjustmentData:


- (BOOL)canHandleAdjustmentData:(PHAdjustmentData *)adjustmentData {
    // Inspect the adjustmentData to determine whether your extension can work with past edits.
    // (Typically, you use its formatIdentifier and formatVersion properties to do this.)
    BOOL result = [adjustmentData.formatIdentifier isEqualToString:@"jp.gaprot.PhotoEditingSample.PhotoEdithing"];
    result &= [adjustmentData.formatVersion isEqualToString:@"1.0"];
    return result;
}

 まずはcanHandleAdjustmentData:メソッドを実装します。

 このメソッドでは BOOL 型を返すのですが、簡単に言うと YES を返せば 画像・動画の再編集が可能となります。

 例えば複数のフィルタから一つを選択してフィルタ加工する Extension アプリの場合、一度フィルタを適用した画像を再度編集しようとしたとします。canHandleAdjustmentData: が YES を返している場合は以前適用したフィルタから別のフィルタへと変更することが可能ですが、NO を返している場合は以前適用されたフィルタの画像に対してさらにフィルタが適用されることとなります。

 引数の adjustmentData についてですが、この変数は PHAdjustmentData クラスのオブジェクトとなっています。PHAdjustmentData は編集した画像や動画データに対して付与されるデータで、再編集を可能にするための情報を設定します。PHAdjustmentData のオブジェクトには識別子(formatIdentifier)とバージョン(formatVersion)、任意のデータ(data)が設定できます。

 後述しますが編集が完了した際には画像や動画に対して PHAdjustmentData を設定するため、写真アプリから再度編集する際に設定された PHAdjustmentData のオブジェクトが引数として渡されます。サンプルコードでは識別子とバージョンを判定して再編集が可能かどうかを判定しています。

startContentEditingWithInput:placeholderImage:


- (void)startContentEditingWithInput:(PHContentEditingInput *)contentEditingInput placeholderImage:(UIImage *)placeholderImage {
    self.input = contentEditingInput;

    UIImage *image = nil;
    switch (self.input.mediaType) {
        case PHAssetMediaTypeImage:
            image = self.input.displaySizeImage;
            break;

        default:
            break;
    }

    // フィルタを適用 -----
    self.sepiaToneFilter = [CIFilter filterWithName:@"CISepiaTone"];
    CIImage *inputImage = [CIImage imageWithCGImage:image.CGImage];
    [self.sepiaToneFilter setValue:inputImage forKey:kCIInputImageKey];

    CIImage *outputImage = self.sepiaToneFilter.outputImage;
    CGImageRef cgImage = [[CIContext contextWithOptions:nil] createCGImage:outputImage fromRect:outputImage.extent];
    UIImage *transformedImage = [UIImage imageWithCGImage:cgImage];
    CGImageRelease(cgImage);
    // ------------------

    self.imageView.image = transformedImage;
}

 startContentEditingWithInput:placeholderImage:メソッドでは、写真アプリから渡された写真や動画のデータが編集可能になった際に呼び出されます。

 引数の contentEditingInputPHContentEditingInput型のオブジェクトで、画像や動画の編集に必要なデータを持っています。

 まずはmediaTypeで画像か動画のデータかを判別しています。(Info.plistで画像のみしか編集しない場合にはこの判定は特に必要ないかもしれません。)

 渡されたデータが画像であった場合には、displaySizeImageプロパティからUIImageを取得します。ここで取得した画像は実サイズの画像ではなく、画面サイズ用にスケールダウンされた画像となっています。

 実は写真アプリから渡される画像データは 2 種類あり、実サイズ画像のURLとディスプレイサイズ用にスケールダウンされた UIImage が取得できます。

 実サイズ画像では画像サイズが大きくなりメモリも多く消費してしまいます。 ですので displaySizeImage に対してフィルタ処理等の画像編集処理を適用しユーザーにプレビュー表示させ、編集が完了した時点で実サイズの画像に対して画像編集処理を行うように処理します。このメソッドでは displaySizeImage に対してセピア調のフィルタ処理を適用して、ImageView で表示しています。

shouldShowCancelConfirmation


- (BOOL)shouldShowCancelConfirmation {

    // Returns whether a confirmation to discard changes should be shown to the user on cancel.
    // (Typically, you should return YES if there are any unsaved changes.)
    return NO;
}

 shouldShowCancelConfirmationはreadonlyのプロパティで、編集をキャンセルする際に確認を行うか否かを設定します。

 YES を返す事で以下の画像のように編集をキャンセルした時に編集内容を破棄するかをユーザーに確認させることができます。NO を返せば編集をキャンセルした時にユーザーに確認せずに編集内容を破棄します。

alt text

cancelContentEditing


- (void)cancelContentEditing {

    // Clean up temporary files, etc.
    // May be called after finishContentEditingWithCompletionHandler: while you prepare output.
}

 cancelContentEditingメソッドは編集のキャンセルが通知された際の処理を記述するためのものです。編集時に使用したリソースの解放処理等を記載します。今回のサンプルでは特に処理を行っておりません。

finishContentEditingWithCompletionHandler:


- (void)finishContentEditingWithCompletionHandler:(void (^)(PHContentEditingOutput *))completionHandler {

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // 出力用のデータを作成.
        PHContentEditingOutput *output = [[PHContentEditingOutput alloc] initWithContentEditingInput:self.input];

        // adjustmentDataの設定 ---
        PHAdjustmentData *adjustmentData = [[PHAdjustmentData alloc] initWithFormatIdentifier:@"jp.gaprot.PhotoEditingSample.PhotoEdithing"
                                                                                formatVersion:@"1.0"
                                                                                         data:nil];
        output.adjustmentData = adjustmentData;
        // -----------------------

        // 実サイズの画像を取得.
        NSURL *url = self.input.fullSizeImageURL;
        UIImage *image = [UIImage imageWithContentsOfFile:url.path];

        // フィルタを適用 -----
        CIImage *inputImage = [CIImage imageWithCGImage:image.CGImage];
        [self.sepiaToneFilter setValue:inputImage forKey:kCIInputImageKey];

        CIImage *outputImage = self.sepiaToneFilter.outputImage;
        CGImageRef cgImage = [[CIContext contextWithOptions:nil] createCGImage:outputImage fromRect:outputImage.extent];
        UIImage *transformedImage = [UIImage imageWithCGImage:cgImage];
        // -----------------

        // 画像を指定のURLに書き出す.
        NSData *renderedJPEGData = UIImageJPEGRepresentation(transformedImage, 0.9f);
        BOOL success = [renderedJPEGData writeToURL:output.renderedContentURL atomically:YES];

        if (success) {
            completionHandler(output);
        } else {
            completionHandler(nil);
        }

        // Clean up temporary files, etc.
    });
}

 finishContentEditingWithCompletionHandler:メソッドは編集が完了した際に呼び出されます。リファレンスにも記載がありますが、このメソッドで処理を記載するにあたっていくつか注意があります。

  1. バックグラウンドキューで残りの処理を実行し、ユーザーがさらに処理を行えないようにUI要素をDisableにする。
  2. startContentEditingWithInput:placeholderImage:メソッドの引数で渡されるPHContentEditingInputのオブジェクトからPHContentEditingOutputのオブジェクトを生成する。
  3. blocksのcompletionHandlerを呼び出す事によって、写真アプリに編集が完了した事を通知する。
  4. blocksのcompletionHandlerを実行した後、編集に使用したデータやファイルの解放処理を行う。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    // ...
});

 まず 1. ですが dispatch_async でバックグラウンドキューで処理を行うようにします。今回 UI は ImageView のみなので特に Disable にする必要もないため、UI に関しては何も処理を行っていません。

// 出力用のデータを作成.
PHContentEditingOutput *output = [[PHContentEditingOutput alloc] initWithContentEditingInput:self.input];

// adjustmentDataの設定 ---
PHAdjustmentData *adjustmentData = [[PHAdjustmentData alloc] initWithFormatIdentifier:@"jp.gaprot.PhotoEditingSample.PhotoEdithing"
                                                                                formatVersion:@"1.0"
                                                                                         data:nil];
output.adjustmentData = adjustmentData;
// -----------------------

 続いて 2. についてですが、PHContentEditingOutput オブジェクトを作成します。引数となっているself.inputですが、これはstartContentEditingWithInput:placeholderImage:メソッドの処理の部分を見ていただけるとわかるかと思いますが、引数で渡されたPHContentEditingInput型のオブジェクトをインスタンス変数として保存しており、そのオブジェクトを使用しています。

 そしてPHContentEditingOutputオブジェクトにはPHAdjustmentDataのオブジェクトを生成して設定しています。先ほど紹介したcanHandleAdjustmentData:メソッドの引数で渡されるadjustmentDataは、ここで設定したものとなります。今回のサンプルではPHAdjustmentDataには識別子とバージョンのみを設定していますが、NSDataで任意のデータを設定する事も可能です。

// 実サイズの画像を取得.
NSURL *url = self.input.fullSizeImageURL;
UIImage *image = [UIImage imageWithContentsOfFile:url.path];

// フィルタを適用 -----
CIImage *inputImage = [CIImage imageWithCGImage:image.CGImage];
[self.sepiaToneFilter setValue:inputImage forKey:kCIInputImageKey];

CIImage *outputImage = self.sepiaToneFilter.outputImage;
CGImageRef cgImage = [[CIContext contextWithOptions:nil] createCGImage:outputImage fromRect:outputImage.extent];
UIImage *transformedImage = [UIImage imageWithCGImage:cgImage];
// -----------------

// 画像を指定のURLに書き出す.
NSData *renderedJPEGData = UIImageJPEGRepresentation(transformedImage, 0.9f);
BOOL success = [renderedJPEGData writeToURL:output.renderedContentURL atomically:YES];

if (success) {
    completionHandler(output);
} else {
    completionHandler(nil);
}

 次に3.についてですが、まずここでフィルタの適用処理を行います。なぜ再度フィルタリングを行うかというと、startContentEditingWithInput:placeholderImage:メソッドで適用したフィルタ処理は画面表示用に行ったものであるからです。実サイズの画像はPHContentEditingInputオブジェクトのfullSizeImageURLに画像が格納されています。

 このファイルパスからUIImageを取得してフィルタ処理を適用します。フィルタ処理を適用した画像はNSData形式に変換してwriteToURL:メソッドで指定のファイルパスに書き出します。 書き出すファイルパスはPHContentEditingOutputオブジェクトのrenderedContentURLプロパティを使用します。

 画像の保存に成功したらblocksのcompletionHandlerを呼び出して処理を完了します。このblocksの引数にはPHContentEditingOutputオブジェクトを設定しますが、ファイルの保存に失敗した場合にはnilを設定します。

 以上でコードの編集は完了となります。

Extensionアプリの起動

 それでは実際に写真アプリからExtensionアプリで画像編集を行ってみたいと思います。

 Photo Editing Extensionはシミュレーターでも確認する事が可能です。下図のように Extension アプリにスキームを合わせた状態でアプリを実行します。

alt text

 実行すると以下のような画面になるので、写真アプリを選択してRunボタンでアプリを起動します。

alt text

 写真アプリが起動したら、任意の画像を選択して右上の編集ボタンをクリックすると下図のような画面になるので、赤線で囲ってある部分をクリックします。

alt text

 クリックすると以下のような画面になるので、今回作成したPhotoEditingSampleを選択します。

alt text

 起動するとフィルタが適用されたプレビュー画面が表示されます。

alt text

 右上の完了ボタンを押すと、フィルタが適用された画像が写真アプリに保存されます。

さいごに

 今回は Photo Editing Extension について紹介させていただきました。この機能拡張は画像だけでなく動画も編集可能なので、興味のある方は動画編集も試してみてはいかがでしょうか。

 また冒頭で紹介した「ファミキャプ」Photo Editing Extension 対応版は近日公開予定ですので、その際にはぜひ遊んでみてください!

参考

App Extensions プログラミングガイド

 Photo Editing ExtensionのサンプルプロジェクトがAppleから公開されています。

Sample Photo Editing Extension

 今回のサンプルプロジェクトではこちらを参考に作成しました。このサンプルでは動画の編集も行っているので、興味のある方はぜひ参考にしてみてください。