個人的なメモ 〜Cocos Sharp 情報を中心に‥

Tomohiro Suzuki @hiro128_777 のブログです。Cocos Sharp の事を中心に書いています。 Microsoft MVP for Visual Studio and Development Technologies 2017- 本ブログと所属組織の公式見解は関係ございません。

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

はじめに

こんにちは、@hiro128_777です。

10月14日(土)と、しばらく先の話になりますが、Xamarin.iOS のハンズオンを開催いたします!

Xcode での開発経験がない方が、iOS の開発を Xamarin で始めてWebで情報を集めると、 Swift や Objective-C の情報はたくさん見つかりますが、Xamarin の情報は案外少ないことに気づきます。
そして、Swift や Objective-C のサンプルコードを見て、Xamarin に移植する必要が出てきますが、これは Xcode での開発経験がないと結構骨が折れる作業です。
そこで今回、Swift, Objective-C のコードを Xamarin.iOS に移植する際のポイントについてハンズオンを行うことにいたしましたので、ご興味がある方はぜひご参加下さい!

jxug.connpass.com


前回は、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ですが、これはnilチェックです。photoSampleBuffernilでなければ、{}内部を実行します。
あとはベタに移植すれば大丈夫です。

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

では、今回はここまでです。

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

はじめに

こんにちは、@hiro128_777です。

10月14日(土)と、しばらく先の話になりますが、Xamarin.iOS のハンズオンを開催いたします!

Xcode での開発経験がない方が、iOS の開発を Xamarin で始めてWebで情報を集めると、 Swift や Objective-C の情報はたくさん見つかりますが、Xamarin の情報は案外少ないことに気づきます。
そして、Swift や Objective-C のサンプルコードを見て、Xamarin に移植する必要が出てきますが、これは Xcode での開発経験がないと結構骨が折れる作業です。
そこで今回、Swift, Objective-C のコードを Xamarin.iOS に移植する際のポイントについてハンズオンを行うことにいたしましたので、ご興味がある方はぜひご参加下さい!

jxug.connpass.com


前回は、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 では残念ながら名称が若干変更されているのでわかりにくくなってしまっています。

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

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


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

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

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

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

はじめに

こんにちは、@hiro128_777です。

10月14日(土)と、しばらく先の話になりますが、Xamarin.iOS のハンズオンを開催いたします!

Xcode での開発経験がない方が、iOS の開発を Xamarin で始めてWebで情報を集めると、 Swift や Objective-C の情報はたくさん見つかりますが、Xamarin の情報は案外少ないことに気づきます。
そして、Swift や Objective-C のサンプルコードを見て、Xamarin に移植する必要が出てきますが、これは Xcode での開発経験がないと結構骨が折れる作業です。
そこで今回、Swift, Objective-C のコードを Xamarin.iOS に移植する際のポイントについてハンズオンを行うことにいたしましたので、ご興味がある方はぜひご参加下さい!

jxug.connpass.com


前回は、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ですがこれは、nil チェックです。
livePhotoCompanionMoviePathnilでなければ、{}内部を実行します

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

	}
}

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

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

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

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

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

はじめに

こんにちは、@hiro128_777です。

10月14日(土)と、しばらく先の話になりますが、Xamarin.iOS のハンズオンを開催いたします!

今まで、Windows の開発を行なっており Xcode での開発経験がない方が、iOS の開発を Xamarin で始めてWebで情報を集めると、 Swift や Objective-C の情報はたくさん見つかりますが、Xamarin の情報は案外少ないことに気づきます。
そして、Swift や Objective-C のサンプルコードを見て、Xamarin に移植する必要が出てきますが、これは Xcode での開発経験がないと結構骨が折れる作業です。
そこで今回、Swift, Objective-C のコードを Xamarin.iOS に移植する際のポイントについてハンズオンを行うことにいたしましたので、ご興味がある方はぜひご参加下さい!

jxug.connpass.com


また、ハンズオン開催に先立って本ブログでも今回から数回にわたり、Swift, Objective-C を Xamarin.iOS に移植する際のポイントをご説明します。

初回である今回は、デリゲートについてご説明します。

C#のデリゲートとiOSのデリゲートの違いについて

C#のデリゲートは安全にカプセル化された関数ポインタのようなもので、言語仕様として準備されている機能ですが、Swiftのデリゲートは言語仕様ではなくデザインパターンであり、設計手法の一つです。よって利用目的は似てるのですが、文法的にはまったく違うものです。

この違いがわかりにくいので、今回はデリゲートについてご説明します。

Xamarin.iOS は、Swift ではなく、Objective-C に寄った設計です。

このことが、Swift のコードを Xamarin.iOS へ移植する際のハードルになっています。もちろん、Swift, Objective-C の開発経験があれば簡単なのですが、Windows の世界から来た方にはなかなか大変です(私もいつも苦労しています)。

プロトコルと Xamarin.iOS

標準ライブラリで定義済みのデリゲートは、プロトコルと一緒に利用されます。

プロトコルは、C#の世界のインターフェースとほぼ同じと考えていただいて結構です。iOSの世界では、標準ライブラリに様々なプロトコルが定義されています。

では、標準ライブラリのプロトコルを利用したデリゲートの実装を詳しく見ていきましょう。

今回は写真撮影のサンプルアプリを題材にします。

以下よりサンプルコードをダウンロードして下さい。
developer.apple.com

その中の、PhotoCaptureDelegate.swiftファイルのコードを見てみましょう。
クラスの定義部分に注目すると、

class PhotoCaptureProcessor: NSObject {
extension PhotoCaptureProcessor: AVCapturePhotoCaptureDelegate {


とあります。

ここに出てきているAVCapturePhotoCaptureDelegateは定義済みのプロトコルです。

Swift でAVCapturePhotoCaptureDelegateのように定義済みのプロトコルが利用されている場合、基本的には Xamarin.iOS 側には対応する interface および class が準備されています。
interface は Swift のプロトコルの名前の頭に 「I」 を付けた名前、class は Swift の Protocol の名前と同名になっています。

例えば、Swift のAVCapturePhotoCaptureDelegateに対応する Xamarin.iOS の interface はIAVCapturePhotoCaptureDelegate、class はAVCapturePhotoCaptureDelegateです。

では、IAVCapturePhotoCaptureDelegateのメタ情報を見てみましょう

namespace AVFoundation
{
	[Introduced(PlatformName.iOS, 10, 0, PlatformArchitecture.None, null)]
	[Protocol(Name = "AVCapturePhotoCaptureDelegate", WrapperType = typeof(AVCapturePhotoCaptureDelegateWrapper))]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "WillBeginCapture", Selector = "captureOutput:willBeginCaptureForResolvedSettings:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(AVCaptureResolvedPhotoSettings) }, ParameterByRef = new[] { false, false })]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "WillCapturePhoto", Selector = "captureOutput:willCapturePhotoForResolvedSettings:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(AVCaptureResolvedPhotoSettings) }, ParameterByRef = new[] { false, false })]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "DidCapturePhoto", Selector = "captureOutput:didCapturePhotoForResolvedSettings:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(AVCaptureResolvedPhotoSettings) }, ParameterByRef = new[] { false, false })]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "DidFinishProcessingPhoto", Selector = "captureOutput:didFinishProcessingPhotoSampleBuffer:previewPhotoSampleBuffer:resolvedSettings:bracketSettings:error:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(CMSampleBuffer), typeof(CMSampleBuffer), typeof(AVCaptureResolvedPhotoSettings), typeof(AVCaptureBracketedStillImageSettings), typeof(NSError) }, ParameterByRef = new[] { false, false, false, false, false, false })]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "DidFinishProcessingRawPhoto", Selector = "captureOutput:didFinishProcessingRawPhotoSampleBuffer:previewPhotoSampleBuffer:resolvedSettings:bracketSettings:error:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(CMSampleBuffer), typeof(CMSampleBuffer), typeof(AVCaptureResolvedPhotoSettings), typeof(AVCaptureBracketedStillImageSettings), typeof(NSError) }, ParameterByRef = new[] { false, false, false, false, false, false })]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "DidFinishRecordingLivePhotoMovie", Selector = "captureOutput:didFinishRecordingLivePhotoMovieForEventualFileAtURL:resolvedSettings:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(NSUrl), typeof(AVCaptureResolvedPhotoSettings) }, ParameterByRef = new[] { false, false, false })]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "DidFinishProcessingLivePhotoMovie", Selector = "captureOutput:didFinishProcessingLivePhotoToMovieFileAtURL:duration:photoDisplayTime:resolvedSettings:error:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(NSUrl), typeof(CMTime), typeof(CMTime), typeof(AVCaptureResolvedPhotoSettings), typeof(NSError) }, ParameterByRef = new[] { false, false, false, false, false, false })]
	[ProtocolMember(IsRequired = false, IsProperty = false, IsStatic = false, Name = "DidFinishCapture", Selector = "captureOutput:didFinishCaptureForResolvedSettings:error:", ParameterType = new[] { typeof(AVCapturePhotoOutput), typeof(AVCaptureResolvedPhotoSettings), typeof(NSError) }, ParameterByRef = new[] { false, false, false })]
	[Unavailable(PlatformName.WatchOS, PlatformArchitecture.All, null)]
	[Unavailable(PlatformName.TvOS, PlatformArchitecture.All, null)]
	[Unavailable(PlatformName.MacOSX, PlatformArchitecture.All, null)]
	public interface IAVCapturePhotoCaptureDelegate : INativeObject, IDisposable
	{
	}
}

かなりごちゃごちゃしていますが、悲しいことに全て非常に重要な情報です(笑)。特にNameSelectorなどの情報に注目してください。


次に、AVCapturePhotoCaptureDelegateのメタ情報を見てみましょう

namespace AVFoundation
{
	[Introduced(PlatformName.iOS, 10, 0, PlatformArchitecture.None, null)]
	[Model]
	[Protocol]
	[Register("AVCapturePhotoCaptureDelegate", false)]
	[Unavailable(PlatformName.WatchOS, PlatformArchitecture.All, null)]
	[Unavailable(PlatformName.TvOS, PlatformArchitecture.All, null)]
	[Unavailable(PlatformName.MacOSX, PlatformArchitecture.All, null)]
	public class AVCapturePhotoCaptureDelegate : NSObject, IAVCapturePhotoCaptureDelegate, INativeObject, IDisposable
	{
		[CompilerGenerated]
		[EditorBrowsable(EditorBrowsableState.Advanced)]
		[Export("init")]
		public AVCapturePhotoCaptureDelegate();

		[CompilerGenerated]
		[EditorBrowsable(EditorBrowsableState.Advanced)]
		protected AVCapturePhotoCaptureDelegate(NSObjectFlag t);

		[CompilerGenerated]
		[EditorBrowsable(EditorBrowsableState.Advanced)]
		protected internal AVCapturePhotoCaptureDelegate(IntPtr handle);

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

こちらにも、非常に重要な情報が含まれています。先ほどのIAVCapturePhotoCaptureDelegateNameSelectorなどの情報と、こちらのExportの情報を組み合わせて、コンパイル時にiOSのデリゲートが構成される仕組みになっています。深堀してみるとかなりの力技ですね(笑)

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

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

次回は、具体的にデリゲートを含むコードをどう移植するのかご説明します。

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

Cocos Sharp での基本的なゲームの制御の方法 複数のアクションを同時に実行する。

はじめに

こんにちは、@hiro128_777です。

今回は複数のアクションを同時に実行する方法についてご説明します。

複数のアクションを同時に実行するアクション

複数のアクションを同時に実行するには、アクションを複数作成した上で、そのアクションを CCSpawn に登録し、CCNodeRunAction メソッドで実行します。

public CCSpawn(params CCFiniteTimeAction[] actions);

例として
移動、拡大、回転
を同時に行うアクションを作成しましょう。

// 連続して実行したいアクションをそれぞれ作成
var moveByAction = new CCMoveBy(3f, new CCPoint(50f, 100f));
var scaleByAction = new CCScaleBy(3f, 2f);
var rotateByAction = new CCRotateBy(3f, 45f);

// 同時実行するアクションを作成
var spawn = new CCSpawn(moveByAction, scaleByAction, rotateByAction);

// アクション実行
node.RunAction(spawn);

これだけで移動、拡大、回転を同時に行うアクションアクションを実行できます。

とても簡単ですね!

今回はここまでです。

Cocos Sharp 画面遷移のアニメーション (7) CCTransitionSplit*, CCTransitionTurnOffTiles, CCTransitionZoomFlip*

はじめに

こんにちは、@hiro128_777です。
今回も、前回に引き続き Cocos Sharp の画面遷移の際のアニメーションについてご紹介します。

前回はCCTransitionRotoZoom, CCTransitionShrinkGrowCCTransitionSlideIn*のアニメーションをご紹介しました。
hiro128.hatenablog.jp

今回はCCTransitionSplit*, CCTransitionTurnOffTilesCCTransitionZoomFlip*をご紹介します。

言葉で表現するよりも見ていただいたほうが早いので、早速アニメーションを実際に見てみましょう。

画面遷移の際のアニメーション ~CCTransitionSplit*~

CCTransitionSplitCols

f:id:hiro128:20170727101253g:plain

CCTransitionSplitRows

f:id:hiro128:20170727101320g:plain

画面遷移の際のアニメーション ~CCTransitionTurnOffTiles~

CCTransitionTurnOffTiles

f:id:hiro128:20170727101347g:plain

画面遷移の際のアニメーション ~CCTransitionZoomFlip*~

CCTransitionZoomFlipAngular (CCTransitionOrientation.DownOver, CCTransitionOrientation.UpOver, CCTransitionOrientation.LeftOver)

f:id:hiro128:20170727101413g:plain

CCTransitionOrientation.DownOver,CCTransitionOrientation.UpOver,CCTransitionOrientation.LeftOverは同じ動きになります。

CCTransitionZoomFlipAngular (CCTransitionOrientation.RightOver)

f:id:hiro128:20170727101444g:plain

CCTransitionZoomFlipX (CCTransitionOrientation.DownOver, CCTransitionOrientation.UpOver, CCTransitionOrientation.LeftOver)

f:id:hiro128:20170727101521g:plain

CCTransitionOrientation.DownOver,CCTransitionOrientation.UpOver,CCTransitionOrientation.LeftOverは同じ動きになります。

CCTransitionZoomFlipX (CCTransitionOrientation.RightOver)

f:id:hiro128:20170727101557g:plain

CCTransitionZoomFlipY (CCTransitionOrientation.DownOver, CCTransitionOrientation.LeftOver, CCTransitionOrientation.RightOver)

f:id:hiro128:20170727101635g:plain

CCTransitionOrientation.DownOver,CCTransitionOrientation.LeftOver,CCTransitionOrientation.RightOverは同じ動きになります。

CCTransitionZoomFlipY (CCTransitionOrientation.UpOver)

f:id:hiro128:20170727101714g:plain


というわけで、色々なアニメーションがありますので、皆さんも、色々試してみてください!

また、画面遷移のアニメーションのご紹介は今回で終了です。

では、今回はここまでです。

Cocos Sharp ハンズオン(2)地図のアニメーション設定

はじめに

こんにちは、@hiro128_777です。


前回は地図が表示される部分までを作成しました。今回はハンズオン用のテキスト後半部分です。ボタンをタップすると地図がアニメーションする部分を作成していきます。

この記事からご覧になった方は、前回からご覧になることをお勧めします。

hiro128.hatenablog.jp

ハンズオン続き

それではさっそく続きを始めましょう。

アニメーションを追加 : GameLayer.cs

GameLayer.csにアニメーション用のフィールドを追加します。

CCActionStateはアニメーションの動作中、終了のステータス管理、CCBlinkCCScaleByなどはアニメーションを表すオブジェクトです。

private CCActionState tokyoActionState;
private CCActionState kanagawaActionState;
private CCActionState saitamaActionState;
private CCActionState chibaActionState;
private CCActionState gunmaActionState;
private CCActionState tochigiActionState;
private CCActionState ibarakiActionState;

private CCBlink blink;
private CCScaleBy scaleLarge;
private CCEaseBackIn easedScaleLarge;
private CCRotateBy rotate;
private CCEaseBounceIn easedRotate;
private CCScaleBy scaleOrigin;

次にGameLayer.csのコンストラクタにアニメーションのインスタンスを作成するコードを追加します。

blink = new CCBlink(2, 10); 
scaleLarge = new CCScaleBy(1, 4); 
easedScaleLarge = new CCEaseBackIn(scaleLarge); 
rotate = new CCRotateBy(3, 2880); 
easedRotate = new CCEaseBounceIn(rotate); 
scaleOrigin = new CCScaleBy(1, 0.25f); 

さらにGameLayer.csに各県ごとの、アニメーションの実行可否判断用のプロパティとアニメーションの実行用のメソッドを追加します。

実行可否判断は現在実行中のアニメーションが終了するまで次のアニメーションを実行させないようにするために使用します。

public bool CanPlayAnimationTokyo
{
	get { return tokyoActionState == null ? true : tokyoActionState.IsDone; }
}

public void PlayAnimationTokyo()
{
	if (!CanPlayAnimationTokyo)
		return;

	var sequence = new CCSequence(
		new CCCallFunc(() => tokyoSprite.ZOrder = 1000),
		blink,
		easedScaleLarge,
		easedRotate,
		scaleOrigin,
		new CCCallFunc(() => tokyoSprite.ZOrder = 0)
		);
	tokyoActionState = tokyoSprite.RunAction(sequence);
}

public bool CanPlayAnimationKanagawa
{
	get { return kanagawaActionState == null ? true : kanagawaActionState.IsDone; }
}

public void PlayAnimationKanagawa()
{
	if (!CanPlayAnimationKanagawa)
		return;

	var sequence = new CCSequence(
		new CCCallFunc(() => kanagawaSprite.ZOrder = 1000),
		blink,
		easedScaleLarge,
		scaleOrigin,
		new CCCallFunc(() => kanagawaSprite.ZOrder = 0)
		);
	kanagawaActionState = kanagawaSprite.RunAction(sequence);
}

public bool CanPlayAnimationSaitama
{
	get { return saitamaActionState == null ? true : saitamaActionState.IsDone; }
}

public void PlayAnimationSaitama()
{
	if (!CanPlayAnimationSaitama)
		return;

	var sequence = new CCSequence(
		new CCCallFunc(() => saitamaSprite.ZOrder = 1000),
		blink,
		scaleLarge,
		rotate,
		scaleOrigin,
		new CCCallFunc(() => saitamaSprite.ZOrder = 0)
		);
	saitamaActionState = saitamaSprite.RunAction(sequence);
}

public bool CanPlayAnimationChiba
{
	get { return chibaActionState == null ? true : chibaActionState.IsDone; }
}

public void PlayAnimationChiba()
{
	if (!CanPlayAnimationChiba)
		return;

	var sequence = new CCSequence(
		new CCCallFunc(() => chibaSprite.ZOrder = 1000),
		easedScaleLarge,
		scaleOrigin,
		new CCCallFunc(() => chibaSprite.ZOrder = 0)
		);
	chibaActionState = chibaSprite.RunAction(sequence);
}

public bool CanPlayAnimationGunma
{
	get { return gunmaActionState == null ? true : gunmaActionState.IsDone; }
}

public void PlayAnimationGunma()
{
	if (!CanPlayAnimationGunma)
		return;

	var sequence = new CCSequence(blink);
	gunmaActionState = gunmaSprite.RunAction(sequence);
}

public bool CanPlayAnimationTochigi
{
	get { return tochigiActionState == null ? true : tochigiActionState.IsDone; }
}

public void PlayAnimationTochigi()
{
	if (!CanPlayAnimationTochigi)
		return;

	var sequence = new CCSequence(
		new CCCallFunc(() => tochigiSprite.ZOrder = 1000),
		scaleLarge,
		scaleOrigin,
		new CCCallFunc(() => tochigiSprite.ZOrder = 0)
		);
	tochigiActionState = tochigiSprite.RunAction(sequence);
}

public bool CanPlayAnimationIbaraki
{
	get { return ibarakiActionState == null ? true : ibarakiActionState.IsDone; }
}

public void PlayAnimationIbaraki()
{
	if (!CanPlayAnimationIbaraki)
		return;

	var sequence = new CCSequence(
		new CCCallFunc(() => ibarakiSprite.ZOrder = 1000),
		rotate,
		new CCCallFunc(() => ibarakiSprite.ZOrder = 0)
		);
	ibarakiActionState = ibarakiSprite.RunAction(sequence);
}


この部分もう少し詳しく解説します。

CanPlayAnimationTokyoプロパティでは、アニメーションのステータスを確認し、アニメーション実行前、または終了後であれば、実行許可を許可します。

public bool CanPlayAnimationTokyo
{
	get { return tokyoActionState == null ? true : tokyoActionState.IsDone; }
}


PlayAnimationTokyoメソッドでは、以下のような処理を行っています。


実行許可が出ていなければアニメーションを実行しません。

if (!CanPlayAnimationTokyo)
	return;

個別のアニメーションを連続実行するように組み合わせ、連続したアニメーションを構築します。

アニメーション時に最前面にないと他のスプライトの後ろに隠れたりしますので、最初に今回アニメーションするスプライトを最前面に移動させ、終了後元に戻しています。

CCSequenceはアニメーションを連続的に実行しますが、CCSpawnでは複数のアニメーションを同時実行できます。

CCEaseBackInなどを使うことによって通常はリニアなアニメーションの変化量を関数的に変化するように装飾できます。

var sequence = new CCSequence(
	new CCCallFunc(() => tokyoSprite.ZOrder = 1000),
	blink,
	easedScaleLarge,
	easedRotate,
	scaleOrigin,
	new CCCallFunc(() => tokyoSprite.ZOrder = 0)
	);

アニメーションをスプライトに紐づけて実行し、実行状況を参照できるようにします。

tokyoActionState = tokyoSprite.RunAction(sequence);

GameLayerの操作メソッドをViewModelに公開する : GameScene.cs

GameSceneGameLayerCanPlayAnimationTokyoなどのアニメーション実行メソッドをViewModelに公開するためのメソッドを追加します。

public void PlayAnimationTokyo()
{
	layer.PlayAnimationTokyo();
}

public void PlayAnimationKanagawa()
{
	layer.PlayAnimationKanagawa();
}

public void PlayAnimationSaitama()
{
	layer.PlayAnimationSaitama();
}

public void PlayAnimationChiba()
{
	layer.PlayAnimationChiba();
}

public void PlayAnimationGunma()
{
	layer.PlayAnimationGunma();
}

public void PlayAnimationTochigi()
{
	layer.PlayAnimationTochigi();
}

public void PlayAnimationIbaraki()
{
	layer.PlayAnimationIbaraki();
}

ViewModelのCommandのデリゲートを設定する : MainPageViewModel.cs

MainPageViewModelDelegateCommandGameSceneのアニメーション起動メソッドを設定します。

public DelegateCommand TokyoCommand => new DelegateCommand(() => GameScene.PlayAnimationTokyo());
public DelegateCommand KanagawaCommand => new DelegateCommand(() => GameScene.PlayAnimationKanagawa());
public DelegateCommand SaitamaCommand => new DelegateCommand(() => GameScene.PlayAnimationSaitama());
public DelegateCommand ChibaCommand => new DelegateCommand(() => GameScene.PlayAnimationChiba());
public DelegateCommand GunmaCommand => new DelegateCommand(() => GameScene.PlayAnimationGunma());
public DelegateCommand TochigiCommand => new DelegateCommand(() => GameScene.PlayAnimationTochigi());
public DelegateCommand IbarakiCommand => new DelegateCommand(() => GameScene.PlayAnimationIbaraki());

確認

以上でプログラムは完成しました!
ボタンをタップすると地図がアニメーションするか試してみましょう。


f:id:hiro128:20170704201300g:plain


いかがでしょうか!これで Cocos Sharp の基本的な使い方はバッチリです!

この後は完成したサンプルをアレンジしてみましょう!

プラクティス

  • アニメーションの種類を変更してみましょう。

CCMoveByCCMoveTo 移動
CCScaleByCCScaleTo 拡大・縮小
CCRotateByCCRotateTo 回転
CCJumpByCCJumpTo ジャンプ
CCFadeInCCFadeOutCCFadeTo フェード
CCTintByCCTintTo 色合いを変化
CCSkewByCCSkewTo ゆがませる
CCBlink 点滅させる


  • CCSequenceの代わりにCCSpawnを使ってみましょう。

 
 

  • 動きにイージングを適用してみましょう。

CCEaseInCCEaseOutCCEaseInOut
CCEaseExponentialInCCEaseExponentialOutCCEaseExponentialInOut
CCEaseSineInCCEaseSineOutCCEaseSineInOut


CCEaseElasticInCCEaseElasticOutCCEaseElasticInOut
CCEaseBounceInCCEaseBounceOutCCEaseBounceInOut
CCEaseBackInCCEaseBackOutCCEaseBackInOut