個人的なメモ

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

10月14日(土)に、Xamarin.iOS のハンズオンを開催いたしました

こんにちは、@hiro128_777です。

10月14日(土)に、Xamarin.iOS のハンズオンを開催いたしました!

jxug.connpass.com

30名以上の方々にご参加いただきました。

メンター陣も強力な方に多数ご協力頂き非常に助かりました。この場をお借りして御礼申し上げます。本当にありがとうございました。

今回はiOSに特化したハンズオンでしたが、躓きが多かったのが予想通り「実機デバッグの環境構築」でした。

特に identifier を Xcode のダミーアプリと Xamarin 側で完全に一致させるところがうまく伝わらずトラブルが多く発生してしまいました。
この部分はテキストを改善したいと考えています。

逆にコーディングではあまり躓きは少なかった印象です。

アンケート結果

ハンズオンを最後まで完了できたか

f:id:hiro128:20171019205253p:plain

強力なメンター陣のおかげで見事全員アプリを動作できました!

こちらも躓きポイントとしては、プログラムコードではなく、実機デバッグの際の環境が整っていなかったケースがほとんどでした。


難易度

f:id:hiro128:20171019205327p:plain

難易度についてですが、約60%の方が「ちょうどいい」とご回答され、約26%の方が「難しい」とご回答されました。

今回は題材的には多少難易度が高めでしたので、その中でも約75%の方から「簡単」または「ちょうどいい」とご回答を頂けたのはとてもうれしい結果となりました。


時間

f:id:hiro128:20171019205351p:plain

時間ですが、80%以上の方が「ちょうどいい」とご回答されました。

ハンズオン自体は、休憩などを省くと正味3時間程度でした。集中力的にも3時間程度がちょうどいい長さだと感じました。

役に立ったか

f:id:hiro128:20171019205426p:plain

なんと95%以上の方に「役に立った」とご回答を頂けました!
主催者としては非常にうれしい結果となりました!

その他のご感想

その他のご感想としては、

  • 実機デバッグにコツが必要なこと
  • SwiftとObjective-Cの勉強の必要性


を挙げられた方が多かったです。

今回のハンズオンのテーマがまさにこの2つでしたので、それを体験頂けたのは非常に良かったと感じております。

今後実施して欲しいハンズオン


今後実施して欲しいハンズオンとしては

  • Xamarin.Forms のハンズオン
  • 同じテーマでの Xamarin.iOS と Xamarin.Androidの比較


というご意見が多く見受けられました。



皆様のご意見を参考に今後も有意義なハンズオンを開催できるように精進いたします。

最後にお忙しい中休日にお時間を作ってご参加頂いた皆様、本当にありがとうございました!

また、JXUGのイベントでお会いできるのを楽しみにしております!

Swift, Objective-C を Xamarin.iOS に移植する際のポイント(2) UIView.Layerの差し替え

はじめに

こんにちは、@hiro128_777です。

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

hiro128.hatenablog.jp

今回は、PreviewView.swiftを Xamarin.iOS へ移植していきましょう。

PreviewView.swift の Xamarin.iOS への移植

では早速swiftのコードを見てみましょう。

Swift

import UIKit
import AVFoundation

class PreviewView: UIView {
    var videoPreviewLayer: AVCaptureVideoPreviewLayer {
        guard let layer = layer as? AVCaptureVideoPreviewLayer else {
            fatalError("Expected `AVCaptureVideoPreviewLayer` type for layer. Check PreviewView.layerClass implementation.")
        }
        
        return layer
    }
    
    var session: AVCaptureSession? {
        get {
            return videoPreviewLayer.session
        }
        set {
            videoPreviewLayer.session = newValue
        }
    }
    
    // MARK: UIView
    
    override class var layerClass: AnyClass {
        return AVCaptureVideoPreviewLayer.self
    }
}

今回、新しく出てきた文法は、override class var layerClass: AnyClass { return AVCaptureVideoPreviewLayer.self }です。

これを Xamarin.iOS に移植するのは少々厄介です。特殊な表現をするので知識として知らないと移植できません。

通常、UIViewLayerは自動的に作成され割り当てられます。
デフォルトではLayerCALayerインスタンスになりますが、override することでCALayer派生の任意の型を使用できます。

Xamarin.iOS で同じことを実現するには、using ObjCRuntime;を追加し、CALayer派生の任意の型を返す静的メソッド、またはプロパティを作成し、[Export("layerClass")]を設定します。

具体的には下記のようになります。

C#

public static Class LayerClass
{
    [Export("layerClass")]
    get
    {
        return layerClass = layerClass ?? new Class(typeof(AVCaptureVideoPreviewLayer));
    }
}

または

[Export ("layerClass")]
public static Class GetLayerClass ()
{
    return new Class(typeof(AVCaptureVideoPreviewLayer));
}

このあたりを詳しくご理解したい場合は、以下の公式ドキュメントを参照してください。
docs.microsoft.com


あとは、そのまま移植するだけです。

using System;

using UIKit;
using Foundation;
using AVFoundation;
using ObjCRuntime;

namespace AVCapture
{
    [Register("PreviewView")]
    public class PreviewView : UIView
    {
        static Class layerClass;

        public static Class LayerClass
        {
            [Export("layerClass")]
            get
            {
                return layerClass = layerClass ?? new Class(typeof(AVCaptureVideoPreviewLayer));
            }
        }

        public AVCaptureSession Session
        {
            get
            {
                return VideoPreviewLayer.Session;
            }
            set
            {
                VideoPreviewLayer.Session = value;
            }
        }

        public AVCaptureVideoPreviewLayer VideoPreviewLayer
        {
            get
            {
                return (AVCaptureVideoPreviewLayer)Layer;
            }
        }

        public PreviewView()
            : base()
        {
        }

    }
}

layerClass の移植方法さえ知っていればあとは非常に簡単です。

今回はここまでです。
 


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

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

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

はじめに

こんにちは、@hiro128_777です。

前回は、Swift のコードPhotoCaptureDelegate.swiftのクラス本体部分を移植しました。

hiro128.hatenablog.jp

今回は、前回の続きです。PhotoCaptureDelegate.swiftのエクステンション部分を Xamarin.iOS へ移植していきましょう。
 

PhotoCaptureDelegate.swift エクステンション部分の Xamarin.iOS への移植

それではPhotoCaptureDelegate.swiftのエクステンション部分のコードを見てみましょう。
 

コールバックメソッドの定義

プロトコルの実装部分を確認してみると以下のようにコールバックのメソッドが定義されています。
※実装の中身は省略しています。

Swift

extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate {
    /*
     This extension includes all the delegate callbacks for AVCapturePhotoCaptureDelegate protocol
    */
    
    func photoOutput(_ output: AVCapturePhotoOutput, willBeginCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
    }
    
    func photoOutput(_ output: AVCapturePhotoOutput, willCapturePhotoFor resolvedSettings: AVCaptureResolvedPhotoSettings) {
    }
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
    }

    func photoOutput(_ output: AVCapturePhotoOutput, didFinishRecordingLivePhotoMovieForEventualFileAt outputFileURL: URL, resolvedSettings: AVCaptureResolvedPhotoSettings) {
    }
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingLivePhotoToMovieFileAt outputFileURL: URL, duration: CMTime, photoDisplayTime: CMTime, resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
    }
    
    func photoOutput(_ output: AVCapturePhotoOutput, didFinishCaptureFor resolvedSettings: AVCaptureResolvedPhotoSettings, error: Error?) {
    }

コールバックメソッドの名前が全てphotoOutputと同じになっています。
ここで、C#の対応するクラスAVCapturePhotoCaptureDelegateのコールバックメソッドのメタ情報を確認してみましょう。

C#

[CompilerGenerated]
[Export("captureOutput:didCapturePhotoForResolvedSettings:")]
public virtual void DidCapturePhoto(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings);

[CompilerGenerated]
[Export("captureOutput:didFinishCaptureForResolvedSettings:error:")]
public virtual void DidFinishCapture(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error);

[CompilerGenerated]
[Export("captureOutput:didFinishProcessingLivePhotoToMovieFileAtURL:duration:photoDisplayTime:resolvedSettings:error:")]
public virtual void DidFinishProcessingLivePhotoMovie(AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, CMTime duration, CMTime photoDisplayTime, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error);

[CompilerGenerated]
[Export("captureOutput:didFinishProcessingPhotoSampleBuffer:previewPhotoSampleBuffer:resolvedSettings:bracketSettings:error:")]
public virtual void DidFinishProcessingPhoto(AVCapturePhotoOutput captureOutput, CMSampleBuffer photoSampleBuffer, CMSampleBuffer previewPhotoSampleBuffer, AVCaptureResolvedPhotoSettings resolvedSettings, AVCaptureBracketedStillImageSettings bracketSettings, NSError error);

[CompilerGenerated]
[Export("captureOutput:didFinishProcessingRawPhotoSampleBuffer:previewPhotoSampleBuffer:resolvedSettings:bracketSettings:error:")]
public virtual void DidFinishProcessingRawPhoto(AVCapturePhotoOutput captureOutput, CMSampleBuffer rawSampleBuffer, CMSampleBuffer previewPhotoSampleBuffer, AVCaptureResolvedPhotoSettings resolvedSettings, AVCaptureBracketedStillImageSettings bracketSettings, NSError error);

[CompilerGenerated]
[Export("captureOutput:didFinishRecordingLivePhotoMovieForEventualFileAtURL:resolvedSettings:")]
public virtual void DidFinishRecordingLivePhotoMovie(AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, AVCaptureResolvedPhotoSettings resolvedSettings);

[CompilerGenerated]
[Export("captureOutput:willBeginCaptureForResolvedSettings:")]
public virtual void WillBeginCapture(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings);

[CompilerGenerated]
[Export("captureOutput:willCapturePhotoForResolvedSettings:")]
public virtual void WillCapturePhoto(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings);

メソッドは全て違う名前になっています。
 

コールバックメソッドの Swift, Xamarin.iOS の対応の判別

ここで Swift と C# のメソッドの定義を良く見比べて下さい。Swiftの第2引数のラベル名にwillBeginCaptureForとあります。C# のWillBeginCaptureメソッドの ExportAttribute を見ると、[Export("captureOutput:willBeginCaptureForResolvedSettings:")]とあります。どちらにも willBeginCaptureForという文字列が含まれているので、このメソッドが対応しているメソッドになります。

Swift

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

C#

[CompilerGenerated]
[Export("captureOutput:willBeginCaptureForResolvedSettings:")]
public virtual void WillBeginCapture(AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings);

微妙に名前が違うのは、何度も言っていますが、Xamarin.iOS が、Objective-C に基づいているからです。

試しに Objective-C のメソッド定義を確認してみましょう。

Objective-C

- (void)captureOutput:(AVCapturePhotoOutput *)captureOutput willBeginCaptureForResolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings
{
}

C# の ExportAttribute [Export("captureOutput:willBeginCaptureForResolvedSettings:")]と、Objective-C のメソッド名captureOutput、第2引数ラベル名willBeginCaptureForResolvedSettingsとなっており見事に名称が一致しています。Swift では残念ながら名称が若干変更されているのでわかりにくくなってしまっています。
 
Xamarin.iOS でのプロトコル、デリゲートの詳細については以下の公式ドキュメントも非常に参考になります。
docs.microsoft.com
 

これで、エクステンションのコールバックメソッドの部分の対応がわかりました。同じ要領で、全てのコールバックメソッドの定義を追加すると以下のようになります。

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)
        {
        }

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

        public override void DidFinishProcessingPhoto (AVCapturePhotoOutput captureOutput, CMSampleBuffer photoSampleBuffer, CMSampleBuffer previewPhotoSampleBuffer, AVCaptureResolvedPhotoSettings resolvedSettings, AVCaptureBracketedStillImageSettings bracketSettings, NSError error)
        {
        }

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

        public override void DidFinishProcessingLivePhotoMovie (AVCapturePhotoOutput captureOutput, NSUrl outputFileUrl, CMTime duration, CMTime photoDisplayTime, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error)
        {
        }

        public override void DidFinishCapture (AVCapturePhotoOutput captureOutput, AVCaptureResolvedPhotoSettings resolvedSettings, NSError error)
        {
        }
    }
}


長くなりましたので今回はここまでです。

間違いなどございましたらご指摘お願いします。

次回は、コールバックメソッドの実装部分を移植します。
 


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

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

はじめに

こんにちは、@hiro128_777です。

前回は、Swift のプロトコル、デリゲートが Xamarin.iOS でどのように表現されているかを確認しました。

hiro128.hatenablog.jp


今回は、前回の続きです。実際に Swift のコードを Xamarin.iOS へ移植していきましょう。

PhotoCaptureDelegate.swift の Xamarin.iOS への移植


それではPhotoCaptureDelegate.swiftファイルのコードを見てみましょう。

using

importを確認すると

Swift

import AVFoundation
import Photos

とありますので、追加します。

C#

using System;
using AVFoundation;
using Photos;

クラス定義

次にクラスの定義部分に注目すると、以下のようにクラスの定義とエクステンションがあります。

Swift

class PhotoCaptureProcessor: NSObject {


Swift

extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate {


前回説明したように、Swift でAVCapturePhotoCaptureDelegateのように定義済みのプロトコルが利用されている場合、基本的には Xamarin.iOS 側には対応する interface および class が準備されています。
よって、AVCapturePhotoCaptureDelegateのメタ情報を確認すると

C#

public class AVCapturePhotoCaptureDelegate : NSObject, IAVCapturePhotoCaptureDelegate, INativeObject, IDisposable

とありますので、AVCapturePhotoCaptureDelegateを継承すれば、NSObjectを継承し、AVCapturePhotoCaptureDelegateを実装するクラスになります。
よって、以下のようにクラスを定義します。

C#

using System;
using AVFoundation;
using Photos;
using Foundation;

namespace AVCamSample
{
    public class PhotoCaptureDelegate : AVCapturePhotoCaptureDelegate
    {
    }
}

 

フィールド

次に、インスタンス変数(C#ではフィールド)を移植します。

Swift

private(set) var requestedPhotoSettings: AVCapturePhotoSettings

private let willCapturePhotoAnimation: () -> Void

private let livePhotoCaptureHandler: (Bool) -> Void

private let completionHandler: (PhotoCaptureProcessor) -> Void

private var photoData: Data?

private var livePhotoCompanionMovieURL: URL?

Swift では[アクセス修飾子] [var or let] [変数名] : [型名] の順で記述されています。

よって、1行目で言えば、requestedPhotoSettingsが変数名、AVCapturePhotoSettingsが型名です。
これは、C#に簡単に書き換えられます。
AVCapturePhotoSettingsの型も Xamarin.iOS に定義済みです。
もし「型が見つからない」とエラーが出る場合は、using を確認してみてください。

private(set) は、setter のみprivateという意味です。

3行目の場合、willCapturePhotoAnimationが変数名、() -> Voidが型名です。
これは、型名が() -> Voidですから、C#で言うデリゲートですね(Swiftではクロージャ)。->の左辺が引数の型、右辺が戻り値の型です。

9,11行目のData?,URL?は、Xamarin.iOS ではそれぞれNSData,NSUrlになります。
NSがつくのは、Xamarin.iOS は、Objective-C に基づいており、Objective-C で、NSがつく型名になっているからです。
また、NSData,NSUrlを使う場合、using Foundation;が必要になります。

これらを考慮するとフィールド定義は以下のようになります。

C#

using System;
using AVFoundation;
using Photos;
using Foundation;

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

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

        NSData photoData;
        NSUrl livePhotoCompanionMovieUrl;
    }
}

 

コンストラクタ

次に、イニシャライザ(C#ではコンストラクタ)を移植します。

Swift

init(with requestedPhotoSettings: AVCapturePhotoSettings,
     willCapturePhotoAnimation: @escaping () -> Void,
     livePhotoCaptureHandler: @escaping (Bool) -> Void,
     completionHandler: @escaping (PhotoCaptureProcessor) -> Void) {
    self.requestedPhotoSettings = requestedPhotoSettings
    self.willCapturePhotoAnimation = willCapturePhotoAnimation
    self.livePhotoCaptureHandler = livePhotoCaptureHandler
    self.completionHandler = completionHandler
}

引数がクロージャであるものに全て@escapingがついていますが、移植する際にはあまり気にしなくても良いです。
※クロージャがスコープから抜けても存在し続けるときに@escapingが必要になります。

selfはC#ではthisです。
あとはベタで移植すれば大丈夫です。

これらを考慮しコンストラクタを追加すると以下のようになります。

C#

using System;
using AVFoundation;
using Photos;
using Foundation;

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;
        }

    }
}

コンストラクタは簡単ですね!
 

メソッド

次に、メソッドを移植します。

Swift

private func didFinish() {
    if let livePhotoCompanionMoviePath = livePhotoCompanionMovieURL?.path {
        if FileManager.default.fileExists(atPath: livePhotoCompanionMoviePath) {
            do {
                try FileManager.default.removeItem(atPath: livePhotoCompanionMoviePath)
            } catch {
                print("Could not remove file at url: \(livePhotoCompanionMoviePath)")
            }
        }
    }
    
    completionHandler(self)
}

ここは多少厄介です。なぜなら、Xamarin.iOS は、Objective-C に基づいており、
Swift をそのまま移植できず、若干表現を変えなければならないためです。

ここは、仕方ないので Swift のコードの処理を理解し、同等の処理を Xamarin.iOS で書かなくてはいけません。

まずif letですがこれは、これは、Optional-Binding と呼ばれており、Optional の Unwrap を行っています。

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

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

FileManagerは、Xamarin.iOS ではNSFileManagerです。
このNSがつくかどうかの件については、慣れるとだんだん迷わなくなります。

最初のうちは「型が見つからない」とエラーが出る場合は、まずは using を確認、
次に、NSをつけてみるという手順を取るとつまづきにくいです。

後は、インテリセンスを利用してNSFileManagerのAPIにあわせて、書き換えていきます。

これらを考慮しDidFinish()を追加すると以下のようになります。

C#

using System;
using AVFoundation;
using Photos;
using Foundation;

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);
        }

    }
}

これでクラス本体部分の移植が完了しました!
いかがでしょうか?最初はちょっとわかりにくいかもしれませんが、何度か試してみれば慣れますので、ぜひ試してみてください!

また、長くなりましたので今回はここまでです。

間違いなどございましたらご指摘お願いします。

次回は、エクステンション部分を移植します。
 


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