個人的なメモ

Tomohiro Suzuki @hiro128_777 のブログです。Microsoft MVP for Developer Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

Swift, Objective-C を Xamarin.iOS に移植する際のポイント(1) デリゲート その4

はじめに

こんにちは、@hiro128_777です。

前回は、Swift のコードPhotoCaptureDelegate.swiftのコールバックメソッドの定義部分を移植しました。

hiro128.hatenablog.jp

今回は、前回の続きです。PhotoCaptureDelegate.swiftのコールバックメソッドの実装を Xamarin.iOS へ移植していきましょう。
 

PhotoCaptureDelegate.swift コールバックメソッドの実装の Xamarin.iOS への移植

 

WillBeginCapture

では、早速、1つ目のコールバックの Swift のコードを見ていきましょう。

Swift

func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
    if resolvedSettings.livePhotoMovieDimensions.width > 0 && resolvedSettings.livePhotoMovieDimensions.height > 0 {
        livePhotoCaptureHandler(true)
    }
}

その1でご説明したように、livePhotoCaptureHandlerに対応するC#のデリゲートはcapturingLivePhotoですので、そこだけ気をつけて書き換えれば、後はベタに移植するだけです。

C#

public override void WillBeginCapture (AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings)
{
    if (resolvedSettings.LivePhotoMovieDimensions.Width > 0 && resolvedSettings.LivePhotoMovieDimensions.Height > 0)
        capturingLivePhoto (true);
}

 

WillCapturePhoto

2つ目です。1つ目同様にC#のデリゲートの対応だけ確認すれば後は簡単です。

Swift

func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
    willCapturePhotoAnimation()
}


C#

public override void WillCapturePhoto (AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings)
{
    willCapturePhotoAnimation ();
}

 

DidFinishProcessingPhoto

3つ目ですが、これは Swift と Xamarin.iOS で既にAPIが違っています。Appleのリファレンスを確認したところ、Swift のサンプルは、iOS11 対応で、 Xamarin.iOS は iOS10 対応のためです。
調べたところjpegPhotoDataRepresentationは iOS11で Deprecated になっていました。

Swift

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    if let error = error {
        print("Error capturing photo: \(error)")
    } else {
        photoData = photo.fileDataRepresentation()
    }
}

このまま移植も可能ですが、わかりにくいので iOS10 対応のサンプルを探したところ以下のようになっていました。これで見比べるとAPIがそろっていますね。

Swift iOS10対応

func capture(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhotoSampleBuffer photoSampleBuffer: CMSampleBuffer?, previewPhotoSampleBuffer: CMSampleBuffer?, resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Error?) {
    if let photoSampleBuffer = photoSampleBuffer {
        photoData = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: photoSampleBuffer, previewPhotoSampleBuffer: previewPhotoSampleBuffer)
    }
    else {
        print("Error capturing photo: \(error)")
        return
    }
}

わかりにくいのは、前にも出てきたif letですが、これは、Optional-Binding と呼ばれており、Optional の Unwrap を行っています。

Optional とは変数にnilの代入を許容するデータ型で、反対に Not Optional はnilを代入できません。Optional の変数にはデータ型の最後に?!をつけます。

Unwrap とは Optionalから Not Optional な値を取り出す事です。

あとはベタに移植すれば大丈夫です。

C#

public override void DidFinishProcessingPhoto (AVCapturePhotoOutput captureOutput, CMSampleBuffer photoSampleBuffer, CMSampleBuffer previewPhotoSampleBuffer, AVCaptureResolvedPhotoSettings resolvedSettings, AVCaptureBracketedStillImageSettings bracketSettings, NSError error)
{
    if (photoSampleBuffer != null)
        photoData = AVCapturePhotoOutput.GetJpegPhotoDataRepresentation (photoSampleBuffer, previewPhotoSampleBuffer);
    else
        Console.WriteLine ($"Error capturing photo: {error.LocalizedDescription}");
}

 

DidFinishRecordingLivePhotoMovie

4つ目です。livePhotoCaptureHandlerに対応するC#のデリゲートはcapturingLivePhotoです。

Swift

func photoOutput(_ output: AVCapturePhotoOutput, didFinishRecordingLivePhotoMovieForEventualFileAt outputFileURL: URL, resolvedSettings: AVCaptureResolvedPhotoSettings) {
    livePhotoCaptureHandler(false)
}

C#

public override void DidFinishRecordingLivePhotoMovie (AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, AVCaptureResolvedPhotoSettings resolvedSettings)
{
    capturingLivePhoto (false);
}

 

DidFinishProcessingLivePhotoMovie

5つ目です。これもベタに移植するだけです。

Swift

func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL, duration: CMTime, photoDisplayTime: CMTime, resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
    if error != nil {
        print("Error processing live photo companion movie: \(String(describing: error))")
        return
    }
    livePhotoCompanionMovieURL = outputFileURL
}

C#

public override void DidFinishProcessingLivePhotoMovie (AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, CMTime duration, CMTime photoDisplayTime, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error)
{
    if (error != null)
    {
        Console.WriteLine ($"Error processing live photo companion movie: {error.LocalizedDescription})");
        return;
    }
    livePhotoCompanionMovieUrl = outputFileUrl;
}

 

DidFinishCapture

6つ目です。こちらはoptions.uniformTypeIdentifier = self.requestedPhotoSettings.processedFileType.map { $0.rawValue } で iOS11 からのAPI、processedFileTypeが使われていました。

Swift

func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
    if let error = error {
        print("Error capturing photo: \(error)")
        didFinish()
        return
    }
    
    guard let photoData = photoData else {
        print("No photo data resource")
        didFinish()
        return
    }
    
    PHPhotoLibrary.requestAuthorization { [unowned self] status in
        if status == .authorized {
            PHPhotoLibrary.shared().performChanges({ [unowned self] in
                let options = PHAssetResourceCreationOptions()
                let creationRequest = PHAssetCreationRequest.forAsset()
                options.uniformTypeIdentifier = self.requestedPhotoSettings.processedFileType.map { $0.rawValue }
                creationRequest.addResource(with: .photo, data: photoData, options: options)
                
                if let livePhotoCompanionMovieURL = self.livePhotoCompanionMovieURL {
                    let livePhotoCompanionMovieFileResourceOptions = PHAssetResourceCreationOptions()
                    livePhotoCompanionMovieFileResourceOptions.shouldMoveFile = true
                    creationRequest.addResource(with: .pairedVideo, fileURL: livePhotoCompanionMovieURL, options: livePhotoCompanionMovieFileResourceOptions)
                }
                
                }, completionHandler: { [unowned self] _, error in
                    if let error = error {
                        print("Error occurered while saving photo to photo library: \(error)")
                    }
                    
                    self.didFinish()
                }
            )
        } else {
            self.didFinish()
        }
    }
}

iOS10 対応のサンプルを探したところ以下のようになっていましたので、こちらを移植していきます。

Swift iOS10対応

func capture(_ captureOutput: AVCapturePhotoOutput, didFinishCaptureForResolvedSettings resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
    if let error = error {
        print("Error capturing photo: \(error)")
        didFinish()
        return
    }
    
    guard let photoData = photoData else {
        print("No photo data resource")
        didFinish()
        return
    }
    
    PHPhotoLibrary.requestAuthorization { [unowned self] status in
        if status == .authorized {
            PHPhotoLibrary.shared().performChanges({ [unowned self] in
                    let creationRequest = PHAssetCreationRequest.forAsset()
                    creationRequest.addResource(with: .photo, data: photoData, options: nil)
                
                    if let livePhotoCompanionMovieURL = self.livePhotoCompanionMovieURL {
                        let livePhotoCompanionMovieFileResourceOptions = PHAssetResourceCreationOptions()
                        livePhotoCompanionMovieFileResourceOptions.shouldMoveFile = true
                        creationRequest.addResource(with: .pairedVideo, fileURL: livePhotoCompanionMovieURL, options: livePhotoCompanionMovieFileResourceOptions)
                    }
                
                }, completionHandler: { [unowned self] success, error in
                    if let error = error {
                        print("Error occurered while saving photo to photo library: \(error)")
                    }
                    
                    self.didFinish()
                }
            )
        }
        else {
            self.didFinish()
        }
    }
}

いくつか、C#erにはなじみのない表現が使われています。

まず、guardですが、guard let photoData = photoData else {}は、アンラップとnilチェックを同時に行い、アンラップしたphotoDataguard~elseブロック外で使用できます。
※アンラップとは、nilを代入できるオプショナル型から値を取り出すことです。

もう一つ、[unowned self]は、非所有参照でselfをキャプチャします。これを使うと、クロージャー内ではクロージャ外のselfとは別の非所有参照のselfを使うのため循環参照が起こりません。

これは、移植時にはメモリリーク防止のおまじないとでも認識しておけば十分です。

PerformChangesメソッドを確認すると以下のようになっていますので、APIにあわせて実装していきます。

C#

public virtual void PerformChanges(Action changeHandler, Action<bool, NSError> completionHandler);


C#

public override void DidFinishCapture(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error)
{
    if (error != null)
    {
        Console.WriteLine($"Error capturing photo: {error.LocalizedDescription})");
        DidFinish();
        return;
    }

    if (photoData == null)
    {
        Console.WriteLine("No photo data resource");
        DidFinish();
        return;
    }

    PHPhotoLibrary.RequestAuthorization(status => {
        if (status == PHAuthorizationStatus.Authorized)
        {
            PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() => {
                var creationRequest = PHAssetCreationRequest.CreationRequestForAsset();
                creationRequest.AddResource(PHAssetResourceType.Photo, photoData, null);

                var url = livePhotoCompanionMovieUrl;
                if (url != null)
                {
                    var livePhotoCompanionMovieFileResourceOptions = new PHAssetResourceCreationOptions
                    {
                        ShouldMoveFile = true
                    };
                    creationRequest.AddResource(PHAssetResourceType.PairedVideo, url, livePhotoCompanionMovieFileResourceOptions);
                }
            }, (success, err) => {
                if (err != null)
                    Console.WriteLine($"Error occurered while saving photo to photo library: {error.LocalizedDescription}");
                DidFinish();
            });
        }
        else
        {
            DidFinish();
        }
    });
}

これで、コールバックメソッドの実装が完了しました。以上でPhotoCaptureDelegateの移植が完了です!お疲れさまでした。


最後に完成したコードは以下のようになります。

C#

using System;

using Foundation;
using AVFoundation;
using CoreMedia;
using Photos;

namespace AVCamSample
{
    public class PhotoCaptureDelegate : AVCapturePhotoCaptureDelegate
    {
        public AVCapturePhotoSettings RequestedPhotoSettings { get; private set; }

        Action willCapturePhotoAnimation;
        Action<bool> capturingLivePhoto;
        Action<PhotoCaptureDelegate> completed;

        NSData photoData;
        NSUrl livePhotoCompanionMovieUrl;


        public PhotoCaptureDelegate(AVCapturePhotoSettings requestedPhotoSettings,
                                     Action willCapturePhotoAnimation,
                                     Action<bool> capturingLivePhoto,
                                     Action<PhotoCaptureDelegate> completed)
        {
            RequestedPhotoSettings = requestedPhotoSettings;
            this.willCapturePhotoAnimation = willCapturePhotoAnimation;
            this.capturingLivePhoto = capturingLivePhoto;
            this.completed = completed;
        }

        void DidFinish()
        {
            var livePhotoCompanionMoviePath = livePhotoCompanionMovieUrl?.Path;
            if (livePhotoCompanionMoviePath != null)
            {
                if (NSFileManager.DefaultManager.FileExists(livePhotoCompanionMoviePath))
                {
                    NSError error;
                    if (!NSFileManager.DefaultManager.Remove(livePhotoCompanionMoviePath, out error))
                        Console.WriteLine($"Could not remove file at url: {livePhotoCompanionMoviePath}");
                }
            }

            completed(this);
        }

        public override void WillBeginCapture (AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings)
        {
            if (resolvedSettings.LivePhotoMovieDimensions.Width > 0 && resolvedSettings.LivePhotoMovieDimensions.Height > 0)
                capturingLivePhoto (true);
        }

        public override void WillCapturePhoto (AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings)
        {
            willCapturePhotoAnimation ();
        }

        public override void DidFinishProcessingPhoto (AVCapturePhotoOutput captureOutput, CMSampleBuffer photoSampleBuffer, CMSampleBuffer previewPhotoSampleBuffer, AVCaptureResolvedPhotoSettings resolvedSettings, AVCaptureBracketedStillImageSettings bracketSettings, NSError error)
        {
            if (photoSampleBuffer != null)
                photoData = AVCapturePhotoOutput.GetJpegPhotoDataRepresentation (photoSampleBuffer, previewPhotoSampleBuffer);
            else
                Console.WriteLine ($"Error capturing photo: {error.LocalizedDescription}");
        }

        public override void DidFinishRecordingLivePhotoMovie (AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, AVCaptureResolvedPhotoSettings resolvedSettings)
        {
            capturingLivePhoto (false);
        }

        public override void DidFinishProcessingLivePhotoMovie (AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, CMTime duration, CMTime photoDisplayTime, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error)
        {
            if (error != null) {
                Console.WriteLine ($"Error processing live photo companion movie: {error.LocalizedDescription})");
                return;
            }

            livePhotoCompanionMovieUrl = outputFileUrl;
        }

        public override void DidFinishCapture (AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error)
        {
            if (error != null)
            {
                Console.WriteLine($"Error capturing photo: {error.LocalizedDescription})");
                DidFinish();
                return;
            }

            if (photoData == null)
            {
                Console.WriteLine("No photo data resource");
                DidFinish();
                return;
            }

            PHPhotoLibrary.RequestAuthorization(status => {
                if (status == PHAuthorizationStatus.Authorized)
                {
                    PHPhotoLibrary.SharedPhotoLibrary.PerformChanges(() => {
                        var creationRequest = PHAssetCreationRequest.CreationRequestForAsset();
                        creationRequest.AddResource(PHAssetResourceType.Photo, photoData, null);

                        var url = livePhotoCompanionMovieUrl;
                        if (url != null)
                        {
                            var livePhotoCompanionMovieFileResourceOptions = new PHAssetResourceCreationOptions
                            {
                                ShouldMoveFile = true
                            };
                            creationRequest.AddResource(PHAssetResourceType.PairedVideo, url, livePhotoCompanionMovieFileResourceOptions);
                        }
                    }, (success, err) => {
                        if (err != null)
                            Console.WriteLine($"Error occurered while saving photo to photo library: {error.LocalizedDescription}");
                        DidFinish();
                    });
                }
                else
                {
                    DidFinish();
                }
            });
        }
        
    }
}

今回はここまでです。
 


次回はこちらからどうぞ!
hiro128.hatenablog.jp