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

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

Swift, Objective-C を Xamarin.iOS に移植する際のポイント(4) storyboard の制約の Xamarin.iOS C#コードへの移植方法

はじめに

こんにちは、@hiro128_777です。

12月9日(土)に、大阪でXamarin.iOS のハンズオンを開催いたします!

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

jxug.connpass.com


前回は、storyboard のUIエレメントの C#コードへの移植方についてご説明しました。

hiro128.hatenablog.jp

そこで、今回は、UIエレメントよりもさらに情報の少ない storyboard の制約のC#コードへの移植方法についてご説明します。

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

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

その中の、Main.storyboardファイルのコードを見てみましょう。

では早速移植していきましょう。

制約の移植

個別のUIエレメント内で完結する制約はUIエレメントを生成する箇所に記載していますので、ここでは、複数のUIエレメントの関係性の制約を移植します。

複数のUIエレメントの関係性の制約はMain.storyboardの161行目~188行目に記述されています。

1つ目の制約を見てみましょう。

<constraint firstItem="3eR-Rn-XpZ" firstAttribute="centerX" secondItem="nyU-fN-aJh" secondAttribute="centerX" id="125-kC-WZF"/>

firstItem, secondItemにそれぞれ文字列が入っていますが、これはそれぞれ特定のUIエレメントを指し示しています。

firstItem3eR-Rn-XpZを検索すると、20行目に、id="3eR-Rn-XpZ"とあります。

<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3eR-Rn-XpZ" userLabel="Preview" customClass="PreviewView" customModule="AVCam" customModuleProvider="target">

よって3eR-Rn-XpZPreviewViewを指し示しています。

同様にsecondItemnyU-fN-aJhを検索すると、189行目に、id="nyU-fN-aJh"とあります。

<viewLayoutGuide key="safeArea" id="nyU-fN-aJh"/>

safeAreaとはiPhone Xを考慮した上下左右のマージンを取った領域です。今回はiOS10対応の移植なので、centerXつまり、左右の中心点を考える上では、Viewと同じ領域と考えて差し支えありません。

よってidをUIエレメントに置き換えて記述すると、以下のようになります。

C#

View.AddConstraint(NSLayoutConstraint.Create(PreviewView, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterX, 1.0f, 0));



同様にその他の制約を移植すると以下のようになります。
※一部制約変更しています。



元のstoryboardの制約

<constraint firstItem="eI6-gV-W7d" firstAttribute="top" secondItem="9i1-NX-Qxg" secondAttribute="bottom" constant="8" id="6iA-0j-auu"/> /* iOS10対応のため変更あり */
<constraint firstItem="eI6-gV-W7d" firstAttribute="centerX" secondItem="nyU-fN-aJh" secondAttribute="centerX" id="ACB-oH-2jU"/>
<constraint firstItem="uCj-6P-mHF" firstAttribute="height" secondItem="eRT-dK-6dM" secondAttribute="height" id="AEV-ew-H4g"/>
<constraint firstItem="Pii-2r-R2l" firstAttribute="top" secondItem="eI6-gV-W7d" secondAttribute="bottom" constant="8" id="B43-ME-uK5"/>
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="height" secondItem="8bC-Xf-vdC" secondAttribute="height" id="Ice-47-M9N"/>
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="rUJ-G6-RPv" secondAttribute="top" id="NFm-e8-abT"/>
<constraint firstItem="FZr-Ip-7WL" firstAttribute="centerX" secondItem="nyU-fN-aJh" secondAttribute="centerX" id="OaZ-uO-vXY"/>
<constraint firstItem="FAC-co-10c" firstAttribute="centerX" secondItem="nyU-fN-aJh" secondAttribute="centerX" id="Oow-A6-mDp"/>
<constraint firstItem="9i1-NX-Qxg" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="top" constant="8" id="PNv-qh-VmU"/> /* 削除 */
<constraint firstItem="zf0-db-esM" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="Ris-mI-8lA"/>
<constraint firstItem="Pii-2r-R2l" firstAttribute="centerX" secondItem="nyU-fN-aJh" secondAttribute="centerX" id="SXi-MU-H9D"/>
<constraint firstItem="zf0-db-esM" firstAttribute="centerX" secondItem="nyU-fN-aJh" secondAttribute="centerX" id="W6q-xJ-jfF"/>
<constraint firstItem="uCj-6P-mHF" firstAttribute="height" secondItem="rUJ-G6-RPv" secondAttribute="height" id="aQi-F7-E2b"/>
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="FAC-co-10c" secondAttribute="bottom" constant="20" id="aSR-Je-0lW"/>
<constraint firstItem="uCj-6P-mHF" firstAttribute="top" secondItem="eRT-dK-6dM" secondAttribute="top" id="bQd-ro-0Hw"/>
<constraint firstItem="nyU-fN-aJh" firstAttribute="bottom" secondItem="uCj-6P-mHF" secondAttribute="bottom" constant="20" id="eWs-co-Aaz"/>
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="igk-MQ-CGt"/>
<constraint firstItem="rUJ-G6-RPv" firstAttribute="leading" secondItem="uCj-6P-mHF" secondAttribute="trailing" constant="20" id="lsk-Hm-rTd"/>
<constraint firstItem="nyU-fN-aJh" firstAttribute="centerX" secondItem="uCj-6P-mHF" secondAttribute="centerX" id="m8a-cF-Rf0"/>
<constraint firstItem="uCj-6P-mHF" firstAttribute="width" secondItem="rUJ-G6-RPv" secondAttribute="width" id="o8j-gw-35B"/>
<constraint firstItem="3eR-Rn-XpZ" firstAttribute="width" secondItem="8bC-Xf-vdC" secondAttribute="width" id="pSC-xP-dl0"/>
<constraint firstItem="uCj-6P-mHF" firstAttribute="width" secondItem="eRT-dK-6dM" secondAttribute="width" id="s8u-Y8-n27"/>
<constraint firstItem="FZr-Ip-7WL" firstAttribute="centerY" secondItem="8bC-Xf-vdC" secondAttribute="centerY" id="sTY-i6-czN"/>
<constraint firstItem="9i1-NX-Qxg" firstAttribute="centerX" secondItem="nyU-fN-aJh" secondAttribute="centerX" id="wWj-VD-34F"/> /* 削除 */
<constraint firstItem="uCj-6P-mHF" firstAttribute="leading" secondItem="eRT-dK-6dM" secondAttribute="trailing" constant="20" id="zwj-TX-t6O"/>


移植後のC#の制約

View.AddConstraint(NSLayoutConstraint.Create(LivePhotoModeButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, View, NSLayoutAttribute.Top, 1.0f, 80f));// iOS10対応のため変更あり
View.AddConstraint(NSLayoutConstraint.Create(LivePhotoModeButton, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterX, 1.0f, 0));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, RecordButton, NSLayoutAttribute.Height, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(CapturingLivePhotoLabel, NSLayoutAttribute.Top, NSLayoutRelation.Equal, LivePhotoModeButton, NSLayoutAttribute.Bottom, 1.0f, 8.0f));
View.AddConstraint(NSLayoutConstraint.Create(PreviewView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, View, NSLayoutAttribute.Height, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, CameraButton, NSLayoutAttribute.Top, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(ResumeButton, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterX, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(CaptureModeControl, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterX, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(CameraUnavailableLabel, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterY, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(CapturingLivePhotoLabel, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterX, 1.0f, 0));
View.AddConstraint(NSLayoutConstraint.Create(CameraUnavailableLabel, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterX, 1.0f, 0));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, CameraButton, NSLayoutAttribute.Height, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, CaptureModeControl, NSLayoutAttribute.Bottom, 1.0f, 20f));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Top, NSLayoutRelation.Equal, RecordButton, NSLayoutAttribute.Top, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(View, NSLayoutAttribute.Bottom, NSLayoutRelation.Equal, PhotoButton, NSLayoutAttribute.Bottom, 1.0f, 20f));
View.AddConstraint(NSLayoutConstraint.Create(PreviewView, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterY, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(CameraButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, PhotoButton, NSLayoutAttribute.Trailing, 1.0f, 20f));
View.AddConstraint(NSLayoutConstraint.Create(View, NSLayoutAttribute.CenterX, NSLayoutRelation.Equal, PhotoButton, NSLayoutAttribute.CenterX, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Width, NSLayoutRelation.Equal, RecordButton, NSLayoutAttribute.Width, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(PreviewView, NSLayoutAttribute.Width, NSLayoutRelation.Equal, View, NSLayoutAttribute.Width, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Width, NSLayoutRelation.Equal, RecordButton, NSLayoutAttribute.Width, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(ResumeButton, NSLayoutAttribute.CenterY, NSLayoutRelation.Equal, View, NSLayoutAttribute.CenterY, 1.0f, 0f));
View.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Leading, NSLayoutRelation.Equal, RecordButton, NSLayoutAttribute.Trailing, 1.0f, 20f));

これで制約の移植が完了しました。

今回はここまでです。

Swift, Objective-C を Xamarin.iOS に移植する際のポイント(3) storyboard の Xamarin.iOS C#コードへの移植方法

はじめに

こんにちは、@hiro128_777です。

12月9日(土)に、大阪でXamarin.iOS のハンズオンを開催いたします!

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

jxug.connpass.com


前回は、UIViewのLayerの差し替えについてご説明しました。

hiro128.hatenablog.jp

今回のお話はUIについてです。Xamarin.iOS では、UIについては storyboard をそのまま利用できます。ですが、実際にアプリを開発すると、storyboard だけで完結するのはなかなか難しく、どうしてもコードでUIを記述する場面が出てきてしまいます。

ところが、Xamarin.iOSでUIをコードで作成する方法の情報は非常に少ないです。そこで、今回は、storyboard の C#コードへの移植方法についてご説明します。

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

以下よりサンプルコードをダウンロードして下さい。
https://developer.apple.com/library/content/samplecode/AVCam/Introduction/Intro.htmldeveloper.apple.com

その中の、Main.storyboardファイルのコードを見てみましょう。

では早速移植していきましょう。

UIエレメントを割り当てるフィールドを追加

CameraViewController.csを作成し、UIエレメントを割り当てるフィールドを追加します。

C#

public class CameraViewController : UIViewController, IAVCaptureFileOutputRecordingDelegate
{
	PreviewView PreviewView { get; set; }
	UILabel CameraUnavailableLabel { get; set; }
	UIButton ResumeButton { get; set; }
	UIButton RecordButton { get; set; }
	UIButton CameraButton { get; set; }
	UIButton PhotoButton { get; set; }
	UIButton LivePhotoModeButton { get; set; }
	UISegmentedControl CaptureModeControl { get; set; }
	UILabel CapturingLivePhotoLabel { get; set; }
}

UIエレメントを構築するメソッドを追加

UIを構築するメソッドを作成します。

C#

private void InitUI()
{
}
View

Main.storyboard 15行目~18行目のViewに関する設定を移植します。

storyboard

<viewController id="BYZ-38-t0r" customClass="CameraViewController" customModule="AVCam" customModuleProvider="target" sceneMemberID="viewController">
    <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>

C#

private void InitUI()
{
	View.ContentMode = UIViewContentMode.ScaleToFill;
	View.Frame = new CGRect(0, 0, 375, 667);
	View.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
}
CameraUnavailableLabel

Main.storyboard 28行目~34行目のCameraUnavailableLabelに関する設定を移植します。

storyboard

<label hidden="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Camera Unavailable" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zf0-db-esM" userLabel="Camera Unavailable">
    <rect key="frame" x="83.5" y="319" width="208" height="29"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
    <fontDescription key="fontDescription" type="system" pointSize="24"/>
    <color key="textColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
    <nil key="highlightedColor"/>
</label>

xmlの各attributeに対応したプロパティを見つけ出し、設定していきます。IntelliSenseをうまく使えば簡単に見つけることができます。

例えばhidden="YES"なら、Hidden = trueuserInteractionEnabled="NO"ならUserInteractionEnabled = falseといった要領です。

わかりにくいプロパティはcolor関係とfontですが、それぞれ、colorはpublic static UIColor FromRGBA(nfloat red, nfloat green, nfloat blue, nfloat alpha);、fontはpublic static UIFont SystemFontOfSize(nfloat size);となります。

全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。

C#

CameraUnavailableLabel = new UILabel
{
	Frame = new CGRect(83.5, 319, 208, 29),
	Hidden = true,
	UserInteractionEnabled = false,
	ContentMode = UIViewContentMode.Left,
	Text = "Camera Unavailable",
	TextAlignment = UITextAlignment.Center,
	LineBreakMode = UILineBreakMode.TailTruncation,
	Lines = 0,
	BaselineAdjustment = UIBaselineAdjustment.AlignBaselines,
	AdjustsFontSizeToFitWidth = false,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 1.0f),
	Font = UIFont.SystemFontOfSize(24f),
	TextColor = UIColor.FromRGBA(1.0f, 1.0f, 0.0f, 1.0f),
};
View.AddSubview(CameraUnavailableLabel);

では、同じ要領で他のUIエレメントも追加していきましょう。

PreviewView

Main.storyboard 20行目~27行目のPreviewViewに関する設定を移植します。

storyboard

<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3eR-Rn-XpZ" userLabel="Preview" customClass="PreviewView" customModule="AVCam" customModuleProvider="target">
    <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
    <gestureRecognizers/>
    <connections>
        <outletCollection property="gestureRecognizers" destination="fY6-qX-ntV" appends="YES" id="G6D-dx-xU8"/>
    </connections>
</view>

InitUI()の先ほど追加したコードの下に以下を追加します。

C#

PreviewView = new PreviewView
{
	Frame = new CGRect(0, 0, 375, 667),
	ContentMode = UIViewContentMode.ScaleToFill,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 1f),
};
View.AddSubview(PreviewView);
PhotoButton

Main.storyboard 68行目~87行目のPhotoButtonに関する設定を移植します。

storyboard

<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uCj-6P-mHF" userLabel="Photo">
    <rect key="frame" x="147.5" y="617" width="80" height="30"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
    <constraints>
        <constraint firstAttribute="height" constant="30" id="NtC-UN-gTs"/>
        <constraint firstAttribute="width" constant="80" id="dxU-UP-4Ae"/>
    </constraints>
    <fontDescription key="fontDescription" type="system" pointSize="20"/>
    <state key="normal" title="Photo">
        <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
    </state>
    <userDefinedRuntimeAttributes>
        <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
            <integer key="value" value="4"/>
        </userDefinedRuntimeAttribute>
    </userDefinedRuntimeAttributes>
    <connections>
        <action selector="capturePhoto:" destination="BYZ-38-t0r" eventType="touchUpInside" id="o5K-SC-fYn"/>
    </connections>
</button>

わかりにくい箇所をご説明しますと、
buttonType="roundedRect"はXamarin.iOSではコンストラクタでの設定となり、PhotoButton = new UIButton(UIButtonType.RoundedRect)となります。
Viewに設定した仮想解像度、幅:375, 高さ:667に対し、PhotoButtonの高さを30に設定しなさいという制約です。
これは、Xamarin.iOSではNSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1.0f, 30)となります。
widthも同じ要領で設定できます。

storyboard

<action selector="capturePhoto:" destination="BYZ-38-t0r" eventType="touchUpInside" id="o5K-SC-fYn"/>

はイベントハンドラの設定です。

CameraViewController.swiftの521行目を確認すると

Swift

@IBAction private func capturePhoto(_ photoButton: UIButton) {

とありますので、これがイベントハンドラです。Xamarin.iOSではイベントが準備されていますので、C#側のハンドラメソッドをCapturePhoto()のように作成し、設定すれば大丈夫です。

全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。

C#

PhotoButton = new UIButton(UIButtonType.RoundedRect)
{
	Frame = new CGRect(147.5, 617, 80, 30),
	Opaque = false,
	ContentMode = UIViewContentMode.ScaleToFill,
	HorizontalAlignment = UIControlContentHorizontalAlignment.Center,
	VerticalAlignment = UIControlContentVerticalAlignment.Center,
	LineBreakMode = UILineBreakMode.MiddleTruncation,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 0.3f),
	Font = UIFont.SystemFontOfSize(20f),
};

PhotoButton.SetTitle("Photo", UIControlState.Normal);
PhotoButton.SetTitleShadowColor(UIColor.FromRGBA(0.5f, 0.5f, 0.5f, 1.0f), UIControlState.Normal);
PhotoButton.Layer.CornerRadius = 4f;
PhotoButton.TouchUpInside += (s, e) => CapturePhoto();
PhotoButton.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1.0f, 30));
PhotoButton.AddConstraint(NSLayoutConstraint.Create(PhotoButton, NSLayoutAttribute.Width, NSLayoutRelation.Equal, 1.0f, 80));
View.AddSubview(PhotoButton);
CameraButton

Main.storyboard 88行目~103行目のCameraButtonに関する設定を移植します。

storyboard

<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="rUJ-G6-RPv" userLabel="Camera">
    <rect key="frame" x="247.5" y="617" width="80" height="30"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
    <fontDescription key="fontDescription" type="system" pointSize="20"/>
    <state key="normal" title="Camera">
        <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
    </state>
    <userDefinedRuntimeAttributes>
        <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
            <integer key="value" value="4"/>
        </userDefinedRuntimeAttribute>
    </userDefinedRuntimeAttributes>
    <connections>
        <action selector="changeCamera:" destination="BYZ-38-t0r" eventType="touchUpInside" id="3W0-h3-6fc"/>
    </connections>
</button>


全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。

C#

CameraButton = new UIButton(UIButtonType.RoundedRect)
{
	Frame = new CGRect(147.5, 617, 80, 30),
	Opaque = false,
	ContentMode = UIViewContentMode.ScaleToFill,
	HorizontalAlignment = UIControlContentHorizontalAlignment.Center,
	VerticalAlignment = UIControlContentVerticalAlignment.Center,
	LineBreakMode = UILineBreakMode.MiddleTruncation,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 0.3f),
	Font = UIFont.SystemFontOfSize(20f),
};

CameraButton.SetTitle("Camera", UIControlState.Normal);
CameraButton.SetTitleShadowColor(UIColor.FromRGBA(0.5f, 0.5f, 0.5f, 1.0f), UIControlState.Normal);
CameraButton.Layer.CornerRadius = 4f;
CameraButton.TouchUpInside += (s, e) => ChangeCamera();
View.AddSubview(CameraButton);
RecordButton

Main.storyboard 52行目~67行目のRecordButtonに関する設定を移植します。

storyboard

<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eRT-dK-6dM" userLabel="Record">
    <rect key="frame" x="47.5" y="617" width="80" height="30"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
    <fontDescription key="fontDescription" type="system" pointSize="20"/>
    <state key="normal" title="Record">
        <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
    </state>
    <userDefinedRuntimeAttributes>
        <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
            <integer key="value" value="4"/>
        </userDefinedRuntimeAttribute>
    </userDefinedRuntimeAttributes>
    <connections>
        <action selector="toggleMovieRecording:" destination="BYZ-38-t0r" eventType="touchUpInside" id="9R7-Ok-FpB"/>
    </connections>
</button>


全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。

C#

RecordButton = new UIButton(UIButtonType.RoundedRect)
{
	Frame = new CGRect(47.5, 617, 80, 30),
	Opaque = false,
	ContentMode = UIViewContentMode.ScaleToFill,
	HorizontalAlignment = UIControlContentHorizontalAlignment.Center,
	VerticalAlignment = UIControlContentVerticalAlignment.Center,
	LineBreakMode = UILineBreakMode.MiddleTruncation,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 0.3f),
	Font = UIFont.SystemFontOfSize(20f),
};

RecordButton.SetTitle("Record", UIControlState.Normal);
RecordButton.SetTitleShadowColor(UIColor.FromRGBA(0.5f, 0.5f, 0.5f, 1f), UIControlState.Normal);
RecordButton.Layer.CornerRadius = 4f;
RecordButton.TouchUpInside += (s, e) => ToggleMovieRecording();
View.AddSubview(RecordButton);
ResumeButton

Main.storyboard 35行目~51行目のResumeButtonに関する設定を移植します。

storyboard

<button hidden="YES" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FZr-Ip-7WL" userLabel="Resume">
    <rect key="frame" x="105" y="314" width="165" height="39"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
    <fontDescription key="fontDescription" type="system" pointSize="24"/>
    <inset key="contentEdgeInsets" minX="10" minY="5" maxX="10" maxY="5"/>
    <state key="normal" title="Tap to resume">
        <color key="titleShadowColor" red="0.5" green="0.5" blue="0.5" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
    </state>
    <userDefinedRuntimeAttributes>
        <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
            <integer key="value" value="4"/>
        </userDefinedRuntimeAttribute>
    </userDefinedRuntimeAttributes>
    <connections>
        <action selector="resumeInterruptedSession:" destination="BYZ-38-t0r" eventType="touchUpInside" id="42K-1B-qJd"/>
    </connections>
</button>


全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。

C#

ResumeButton = new UIButton(UIButtonType.RoundedRect)
{
	Frame = new CGRect(105, 314, 165, 39),
	Hidden = true,
	Opaque = false,
	ContentMode = UIViewContentMode.ScaleToFill,
	HorizontalAlignment = UIControlContentHorizontalAlignment.Center,
	VerticalAlignment = UIControlContentVerticalAlignment.Center,
	LineBreakMode = UILineBreakMode.MiddleTruncation,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 0.3f),
	Font = UIFont.SystemFontOfSize(24f),
};
ResumeButton.SetTitle("Tap to resume", UIControlState.Normal);
ResumeButton.SetTitleShadowColor(UIColor.FromRGBA(0.5f, 0.5f, 0.5f, 1f), UIControlState.Normal);
ResumeButton.Layer.CornerRadius = 4f;
ResumeButton.TouchUpInside += (s, e) => ResumeInterruptedSession();
View.AddSubview(ResumeButton);
CaptureModeControl

Main.storyboard 104行目~113行目のCaptureModeControlに関する設定を移植します。

今度はSegmentedControlですが、要領は同じです。

storyboard

<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="FAC-co-10c" userLabel="Capture Mode">
    <rect key="frame" x="136" y="569" width="103" height="29"/>
    <segments>
        <segment title="Photo"/>
        <segment title="Movie"/>
    </segments>
    <connections>
        <action selector="toggleCaptureMode:" destination="BYZ-38-t0r" eventType="valueChanged" id="SKd-67-ZHh"/>
    </connections>
</segmentedControl>


ちょっとわかりにくい箇所としては、

<segments>
    <segment title="Photo"/>
    <segment title="Movie"/>
</segments>
```

の部分は、<code>InsertSegment</code>メソッドが準備されているので、それを使うと以下のようになります。

C#
>|cs|
CaptureModeControl.InsertSegment("Photo", 0, true);
CaptureModeControl.InsertSegment("Movie", 1, true);

このあたりのコンストラクタ引数なのか、プロパティなのか、メソッドなのかというさじ加減も慣れると迷わなくなります。

全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。

C#

CaptureModeControl = new UISegmentedControl
{
	Frame = new CGRect(136, 569, 103, 29),
	Opaque = false,
	ContentMode = UIViewContentMode.ScaleToFill,
	HorizontalAlignment = UIControlContentHorizontalAlignment.Left,
	ControlStyle = UISegmentedControlStyle.Plain,
	VerticalAlignment = UIControlContentVerticalAlignment.Top,
	TranslatesAutoresizingMaskIntoConstraints = false,
};
CaptureModeControl.InsertSegment("Photo", 0, true);
CaptureModeControl.InsertSegment("Movie", 1, true);
CaptureModeControl.SelectedSegment = 0;
CaptureModeControl.ValueChanged += (s, e) => ToggleCaptureMode();
View.AddSubview(CaptureModeControl);
LivePhotoModeButton

Main.storyboard 130行目~146行目のLivePhotoModeButtonに関する設定を移植します。

storyboard

<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="eI6-gV-W7d" userLabel="Live Photo Mode">
    <rect key="frame" x="96.5" y="41" width="182" height="25"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
    <constraints>
        <constraint firstAttribute="height" constant="25" id="om7-Gh-HVl"/>
    </constraints>
    <fontDescription key="fontDescription" type="system" pointSize="20"/>
    <state key="normal" title="Live Photo Mode: On"/>
    <userDefinedRuntimeAttributes>
        <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
            <integer key="value" value="4"/>
        </userDefinedRuntimeAttribute>
    </userDefinedRuntimeAttributes>
    <connections>
        <action selector="toggleLivePhotoMode:" destination="BYZ-38-t0r" eventType="touchUpInside" id="JqX-wJ-Xf1"/>
    </connections>
</button>


全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。


C#

LivePhotoModeButton = new UIButton(UIButtonType.RoundedRect)
{
	Frame = new CGRect(96.5, 41, 182, 25),
	Opaque = false,
	ContentMode = UIViewContentMode.ScaleToFill,
	HorizontalAlignment = UIControlContentHorizontalAlignment.Center,
	VerticalAlignment = UIControlContentVerticalAlignment.Center,
	LineBreakMode = UILineBreakMode.MiddleTruncation,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 0.3f),
	Font = UIFont.SystemFontOfSize(20f),
};

LivePhotoModeButton.SetTitle("Live Photo Mode: On", UIControlState.Normal);
LivePhotoModeButton.SetTitleShadowColor(UIColor.FromRGBA(0.5f, 0.5f, 0.5f, 1f), UIControlState.Normal);
LivePhotoModeButton.Layer.CornerRadius = 4f;
LivePhotoModeButton.TouchUpInside += (s, e) => ToggleLivePhotoMode();

LivePhotoModeButton.AddConstraint(NSLayoutConstraint.Create(LivePhotoModeButton, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1.0f, 25));

View.AddSubview(LivePhotoModeButton);
CapturingLivePhotoLabel

Main.storyboard 147行目~158行目のLivePhotoModeButtonに関する設定を移植します。

storyboard

<label hidden="YES" opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Live" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Pii-2r-R2l" userLabel="Capturing Live Photo">
    <rect key="frame" x="172" y="74" width="31" height="20.5"/>
    <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.29999999999999999" colorSpace="custom" customColorSpace="sRGB"/>
    <fontDescription key="fontDescription" type="system" pointSize="17"/>
    <color key="textColor" red="1" green="1" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
    <nil key="highlightedColor"/>
    <userDefinedRuntimeAttributes>
        <userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
            <integer key="value" value="4"/>
        </userDefinedRuntimeAttribute>
    </userDefinedRuntimeAttributes>
</label>


全部移植すると以下のようになりますので、InitUI()の先ほど追加したコードの下に以下を追加します。


C#

CapturingLivePhotoLabel = new UILabel
{
	Frame = new CGRect(172, 74, 31, 20.5),
	Hidden = true,
	Opaque = false,
	UserInteractionEnabled = false,
	ContentMode = UIViewContentMode.Left,
	Text = "Live",
	TextAlignment = UITextAlignment.Center,
	LineBreakMode = UILineBreakMode.TailTruncation,
	Lines = 0,
	BaselineAdjustment = UIBaselineAdjustment.AlignBaselines,
	AdjustsFontSizeToFitWidth = false,
	TranslatesAutoresizingMaskIntoConstraints = false,
	BackgroundColor = UIColor.FromRGBA(0.0f, 0.0f, 0.0f, 0.3f),
	Font = UIFont.SystemFontOfSize(17f),
	TextColor = UIColor.FromRGBA(1.0f, 1.0f, 0.0f, 1.0f),
};

CapturingLivePhotoLabel.Layer.CornerRadius = 4f;

CapturingLivePhotoLabel.AddConstraint(NSLayoutConstraint.Create(CapturingLivePhotoLabel, NSLayoutAttribute.Height, NSLayoutRelation.Equal, 1.0f, 25));
CapturingLivePhotoLabel.AddConstraint(NSLayoutConstraint.Create(CapturingLivePhotoLabel, NSLayoutAttribute.Width, NSLayoutRelation.Equal, 1.0f, 40));

View.AddSubview(CapturingLivePhotoLabel);

これでUIエレメントの移植が完了しました。

今回はここまでです。

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です。

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

今回は、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));
}

このあたりを詳しくご理解したい方はこちらを参照してください。

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

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の移植方法さえ知っていればあとは非常に簡単です。

今回はここまでです。

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

はじめに

こんにちは、@hiro128_777です。

12月9日(土)に、大阪で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です。

12月9日(土)に、大阪で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です。

12月9日(土)に、大阪で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);
		}

	}
}

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

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

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

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