iOSHandsOn - P3PPP/NakayokunaruHandsOn GitHub Wiki
Xamarin.Forms(PCL)で新規プロジェクトを作ります。
プロジェクト名は「NakayokunaruHandsOn」にするとサンプルとnamespaceが一致するのでスムーズです。 プロジェクトを作成したら、そのままビルドして実行できることを確認しましょう。
今回のハンズオンではXamarin.Formsの要求バージョンがとても低いので、NuGetパッケージは更新しなくても問題ありません。
PCLプロジェクトでの作業です。 Appクラスのコンストラクタを以下のように書き換えてください。 (ファイル名はプロジェクトテンプレートによって異なりますが「App.cs」または「NakayokunaruHandsOn.cs」となっているはずです。) これは動作確認時のエントリーページになります。 コメントアウト箇所は進行状況によって適宜解除します。
public App()
{
var rootPage = new ContentPage();
var toRoundedBoxView = new Button
{
Text = "RoundedBoxView"
};
// toRoundedBoxView.Clicked += async (s, e) =>
// await rootPage.Navigation.PushAsync(new RoundedBoxViewPage());
var toEventBasedWebView = new Button
{
Text = "EventBasedWebView"
};
// toEventBasedWebView.Clicked += async (s, e) =>
// await rootPage.Navigation.PushAsync(new EventBasedWebViewPage());
var toMessageBasedWebView = new Button
{
Text = "MessageBasedWebView"
};
// toMessageBasedWebView.Clicked += async (s, e) =>
// await rootPage.Navigation.PushAsync(new MessageBasedWebViewPage());
rootPage.Content = new StackLayout
{
VerticalOptions = LayoutOptions.Center,
Children =
{
toRoundedBoxView,
toEventBasedWebView,
toMessageBasedWebView,
},
};
MainPage = new NavigationPage(rootPage);
}
ここでいったんプロジェクトビルドしてを実行してみましょう。 縦に3つのボタンが並んだページが表示されればオーケーです。
BoxViewもどきを作りつつXamarin.Formsコントロールのプロパティ値変更をネイティブコントロールに伝える方法を確認します。
PCLプロジェクトでの作業です。
新しく「RoundedBoxView.cs」を追加、以下の内容に書き換えます。 コメントアウト箇所は後の工程の部分です。
using System;
using Xamarin.Forms;
namespace NakayokunaruHandsOn
{
public class RoundedBoxView : View
{
#region CornerRadius BindableProperty
public static readonly BindableProperty CornerRadiusProperty =
BindableProperty.Create(nameof(CornerRadius), typeof(double), typeof(RoundedBoxView), 5.0);
public double CornerRadius
{
get { return (double)GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
#endregion
#region Color BindableProperty
public static readonly BindableProperty ColorProperty =
BindableProperty.Create(nameof(Color), typeof(Color), typeof(RoundedBoxView), Color.Accent);
public Color Color
{
get { return (Color)GetValue(ColorProperty); }
set { SetValue(ColorProperty, value); }
}
#endregion
// public event EventHandler Clicked;
// internal void SendClick()
// {
// Clicked?.Invoke(this, EventArgs.Empty);
// }
}
}
BindablePropertyの定義にはコードスニペットを使うと活用すると便利です。
【Xamarin.Forms】BindableProperty.Create() non-generic版のコードスニペット - ぴーさんログ
iOSプロジェクトでの作業です。
新しく「RoundedBoxViewRenderer.cs」を追加、以下の内容に書き換えます。 コメントアウト箇所は後の工程の部分です。
using System.ComponentModel;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
// Xamarin.FormsコントロールとRendererの対応を宣言
[assembly: ExportRenderer(typeof(NakayokunaruHandsOn.RoundedBoxView), typeof(NakayokunaruHandsOn.iOS.RoundedBoxViewRenderer))]
namespace NakayokunaruHandsOn.iOS
{
public class RoundedBoxViewRenderer : ViewRenderer<RoundedBoxView, UIView>
{
// private UITapGestureRecognizer tapGesuture;
protected override void OnElementChanged(ElementChangedEventArgs<RoundedBoxView> e)
{
if (Control == null)
{
var nativeControl = new UIView();
// tapGesuture = new UITapGestureRecognizer(() => Element?.SendClick());
// nativeControl.AddGestureRecognizer(tapGesuture);
SetNativeControl(nativeControl);
}
if (e.NewElement != null)
{
UpdateRadius();
UpdateColor();
}
base.OnElementChanged(e);
}
protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
{
base.OnElementPropertyChanged(sender, e);
if (e.PropertyName == RoundedBoxView.CornerRadiusProperty.PropertyName)
{
UpdateRadius();
}
if (e.PropertyName == RoundedBoxView.ColorProperty.PropertyName)
{
UpdateColor();
}
}
private void UpdateRadius()
{
Control.Layer.CornerRadius = (float)Element.CornerRadius;
}
private void UpdateColor()
{
Control.BackgroundColor = Element.Color.ToUIColor();
}
// protected override void Dispose(bool disposing)
// {
// if (Control != null)
// Control.RemoveGestureRecognizer(tapGesuture);
// base.Dispose(disposing);
// }
}
}
OnElementChangedでネイティブコントロールを生成します。 OnElementPropertyChangedで変化のあったプロパティ名が渡されるので、ここで対応するネイティブコントロールのプロパティに反映させます。
ExportRendererAttributeでXamarin.FormsコントロールとRendererの対応を宣言します。
PCLプロジェクトでの作業です。
新しくContentPage「RoundedBoxViewPage」を追加、以下の内容に書き換えます。
RoundedBoxViewPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:NakayokunaruHandsOn"
Title="RoundedBoxView"
x:Class="NakayokunaruHandsOn.RoundedBoxViewPage">
<StackLayout HorizontalOptions="Center"
VerticalOptions="Center">
<local:RoundedBoxView x:Name="roundedBox"
HeightRequest="100"
WidthRequest="100"
CornerRadius="10"/>
<Button Text="Next Color" Clicked="OnClicked" />
</StackLayout>
</ContentPage>
RoundedBoxViewPage.xaml.cs
using System;
using Xamarin.Forms;
namespace NakayokunaruHandsOn
{
public partial class RoundedBoxViewPage : ContentPage
{
public RoundedBoxViewPage()
{
InitializeComponent();
}
Random random = new Random ();
private void OnClicked (object sender, EventArgs e)
{
roundedBox.Color = Color.FromRgb (
random.Next (255),
random.Next (255),
random.Next (255));
}
}
}
Appクラスのコンストラクタ以下の部分のコメントアウトを解除します。
toRoundedBoxView.Clicked += async (s, e) =>
await rootPage.Navigation.PushAsync(new RoundedBoxViewPage());
ここまで出来たらプロジェクトをビルドして実行してみましょう。 エントリーページで「RoundedBoxView」を押して動作確認ページに移ります。 角丸四角形と「Next Color」ボタンが表示され、「Next Color」ボタンを押すたびに角丸四角形の色が変化すればオーケーです。
ここまで作ってきたRoundedBoxViewにクリックイベントを追加します。
PCLプロジェクトでの作業です。
「RoundedBoxView.cs」を追加した際、コメントアウトしていた部分を解除します。
public event EventHandler Clicked;
internal void SendClick()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
Xamarin.Formsコントロールにイベントを生やしただけでは永久に発火しないので、Rendererにキックしてもらうためのinternalメソッドを用意しておきます。 EventArgsに何かしらの情報を持たせる場合はinternalメソッドのパラメータで受け取ればオーケーです。
さて、各プラットフォームのRendererはPCLとは別のassemblyなのでこのままではinternalメンバーにアクセスできません。 そこでInternalsVisibleToAttributeを使って明示的に別assemblyからのアクセスを許可します。
AssemblyInfo.cs に以下のコードを追加してください。
[assembly: InternalsVisibleTo("NakayokunaruHandsOn.iOS")]
[assembly: InternalsVisibleTo("NakayokunaruHandsOn.Droid")]
[assembly: InternalsVisibleTo("NakayokunaruHandsOn.UWP")]
2016年7月現在、iOSプロジェクトファイルでassembly名の「.」が勝手に削除される問題が確認されています。 発生するとビルドが失敗するようになるので、InternalsVisibleToで指定する名前を"NakayokunaruHandsOniOS"に変更するか、 プロジェクトファイルの方を適宜修正するかしてください。
iOSプロジェクトでの作業です。
「RoundedBoxViewRenderer.cs」を追加した際、コメントアウトしていた部分を解除します。
private UITapGestureRecognizer tapGesuture;
protected override void OnElementChanged(ElementChangedEventArgs<RoundedBoxView> e)
{
(略)
tapGesuture = new UITapGestureRecognizer(() => Element?.SendClick());
nativeControl.AddGestureRecognizer(tapGesuture);
(略)
}
(略)
protected override void Dispose(bool disposing)
{
if (Control != null)
Control.RemoveGestureRecognizer(tapGesuture);
base.Dispose(disposing);
}
PCLプロジェクトでの作業です。
「RoundedBoxViewPage.xaml」のRoundedBoxViewにClickedイベントハンドラを追加します。 コードビハインドはButtonで使ったものを共用するので変更ありません。
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:NakayokunaruHandsOn"
Title="RoundedBoxView"
x:Class="NakayokunaruHandsOn.RoundedBoxViewPage">
<StackLayout HorizontalOptions="Center"
VerticalOptions="Center">
<local:RoundedBoxView x:Name="roundedBox"
HeightRequest="100"
WidthRequest="100"
CornerRadius="10"
Clicked="OnClicked"/>
<Button Text="Next Color" Clicked="OnClicked" />
</StackLayout>
</ContentPage>
ここまで出来たらプロジェクトをビルドして実行してみましょう。 エントリーページで「RoundedBoxView」を押して動作確認ページに移ります。 今度は角丸四角形をタップしても角丸四角形の色が変化するようになっているはずです。
WebViewのAPIの一部を自分で実装しつつ、Xamarin.FormsコントロールからNativeコントロールのAPIを呼ぶ方法を学びます。 具体的には、 ・パラメータの無いAPIとしてナビゲーション機能の「戻る」、「進む」をXamarin.Formsをから呼び出します。 ・パラメータが必要なAPIとしてJavascriptのEval機能をXamarin.Formsをから呼び出します。
今回は2パターンの実装方法を紹介します。(時間的な問題で片方省くかも...)
・Xamarin.Formsコントロールにinternalイベントを用意してRendererに購読させるパターン ・Xamarin.FormsコントロールからMessagingCenter経由でRendererにメッセージを送るパターン
どちらのアプローチでも同等のことができます。 (internalイベントは本家WebView、MessagingCenterは本家Mapで使われている実装です)
App Transport Securityの無効化
動作確認でインターネットに接続するのでATSを無効にしておきます。
info.plist に以下の設定を追加してください
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
:
(省略)
:
<!-- ここを追加 -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
- Xamarin.Formsコントロールにinternalイベントを定義
- Rendererでinternalイベントを購読
- Xamarin.Formsコントロールに定義したメソッドの内部でイベントをInvokeします。
- RendererのイベントハンドラでネイティブコントロールのAPIを呼びます。
これでイベントを経由してXamarin.FormsコントロールからネイティブコントロールのAPIを呼ぶことができます。 パラメータが必要な場合はカスタムEventArgsを定義します。
PCLプロジェクトでの作業です。
新しく「EventBasedWebView.cs」を追加、以下の内容に書き換えます。 RoundedBoxViewと同様にInternalVisibleToAttributeが必要です。
using System;
using Xamarin.Forms;
namespace NakayokunaruHandsOn
{
public class EventBasedWebView : View
{
public void GoBack()
{
EventHandler handler = GoBackRequested;
if (handler != null)
{
handler.Invoke(this, EventArgs.Empty);
}
}
public void GoForward()
{
EventHandler handler = GoForwardRequested;
if (handler != null)
{
handler.Invoke(this, EventArgs.Empty);
}
}
public void Eval(string script)
{
EventHandler<EvalRequestedEventArgs> handler = EvalRequested;
if (handler != null)
{
handler.Invoke(this, new EvalRequestedEventArgs(script));
}
}
internal event EventHandler GoBackRequested;
internal event EventHandler GoForwardRequested;
internal event EventHandler<EvalRequestedEventArgs> EvalRequested;
}
public class EvalRequestedEventArgs
{
public string Script
{
get;
private set;
}
public EvalRequestedEventArgs(string script)
{
Script = script;
}
}
}
iOSプロジェクトでの作業です。
新しく「EventBasedWebViewRenderer.cs」を追加、以下の内容に書き換えます。
using System;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using Foundation;
[assembly: ExportRenderer(typeof(NakayokunaruHandsOn.EventBasedWebView), typeof(NakayokunaruHandsOn.iOS.EventBasedWebViewRenderer))]
namespace NakayokunaruHandsOn.iOS
{
public class EventBasedWebViewRenderer : ViewRenderer<EventBasedWebView, UIWebView>
{
protected override void OnElementChanged(ElementChangedEventArgs<EventBasedWebView> e)
{
if (Control == null)
{
var nativeControl = new UIWebView();
nativeControl.LoadRequest(new NSUrlRequest(new NSUrl("http://ticktack.hatenablog.jp/entry/2016/06/11/124751")));
SetNativeControl(nativeControl);
}
if (e.OldElement != null)
{
Element.GoBackRequested -= OnGoBackRequested;
Element.GoForwardRequested -= OnGoForwardRequested;
Element.EvalRequested -= OnEvalRequested;
}
if (e.NewElement != null)
{
Element.GoBackRequested += OnGoBackRequested;
Element.GoForwardRequested += OnGoForwardRequested;
Element.EvalRequested += OnEvalRequested;
}
base.OnElementChanged(e);
}
private void OnGoBackRequested(object sender, EventArgs e)
{
if (Control.CanGoBack)
{
Control.GoBack();
}
}
private void OnGoForwardRequested(object sender, EventArgs e)
{
if (Control.CanGoForward)
{
Control.GoForward();
}
}
private void OnEvalRequested(object sender, EvalRequestedEventArgs e)
{
Control.EvaluateJavascript(e.Script);
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
Control.Frame = this.Bounds;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
Element.GoBackRequested -= OnGoBackRequested;
Element.GoForwardRequested -= OnGoForwardRequested;
Element.EvalRequested -= OnEvalRequested;
}
base.Dispose(disposing);
}
}
}
PCLプロジェクトでの作業です。
新しくContentPage「EventBasedWebViewPage」を追加、以下の内容に書き換えます。
EventBasedWebViewPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:NakayokunaruHandsOn"
Title="EventBasedWebView"
x:Class="NakayokunaruHandsOn.EventBasedWebViewPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:EventBasedWebView Grid.Row="0" x:Name="webView" />
<StackLayout Grid.Row="1" Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Button Text="戻る" Clicked="GoBackClicked"/>
<Button Text="進む" Clicked="GoForwardClicked"/>
</StackLayout>
<StackLayout Grid.Row="2" Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Entry Text="location.reload()" x:Name="entry" HorizontalOptions="FillAndExpand"/>
<Button Text="Eval" Clicked="EvalClicked"/>
</StackLayout>
</Grid>
</ContentPage>
EventBasedWebViewPage.xaml.cs
using Xamarin.Forms;
namespace NakayokunaruHandsOn
{
public partial class EventBasedWebViewPage : ContentPage
{
public EventBasedWebViewPage()
{
InitializeComponent();
}
void GoBackClicked(object sender, System.EventArgs e)
{
webView.GoBack();
}
void GoForwardClicked(object sender, System.EventArgs e)
{
webView.GoForward();
}
void EvalClicked(object sender, System.EventArgs e)
{
webView.Eval(entry.Text);
}
}
}
Appクラスのコンストラクタで以下の部分のコメントアウトを解除します。
toEventBasedWebView.Clicked += async (s, e) =>
await rootPage.Navigation.PushAsync(new EventBasedWebViewPage());
ここまで出来たらプロジェクトをビルドして実行してみましょう。 エントリーページで「EventBasedWebView」を押して動作確認ページに移ります。 適当にWebページ内のリンクを開いた後、「進む」「戻る」ボタンが機能することを確認してください。 Entryコントロールの初期値として、JavascriptのEval確認用にページをリロードするコードが入っています。 「Eval」ボタンを押すとページが再読み込みすることを確認してください。
こちらのパターンは素直な実装です。
- Xamarin.FormsコントロールからMessagingCenterにメッセージを送信
- Rendererでメッセージを受信、ネイティブコントロールのAPIを呼びます。
RendererでメッセージをSubscribeする際にsenderのインスタンスを与えることで、 Rendererと対応したXamarin.Formsコントロールからのメッセージだけを受け取るようにします。
PCLプロジェクトでの作業です。
新しく「MessageBasedWebView.cs」を追加、以下の内容に書き換えます。 RoundedBoxViewと同様にInternalVisibleToAttributeが必要です。
using Xamarin.Forms;
namespace NakayokunaruHandsOn
{
public class MessageBasedWebView : View
{
internal static readonly string GoBackKey = "MessageBasedWebView.GoBack";
internal static readonly string GoForwardKey = "MessageBasedWebView.GoForward";
internal static readonly string EvalKey = "MessageBasedWebView.Eval";
public void GoBack()
{
MessagingCenter.Send(this, GoBackKey);
}
public void GoForward()
{
MessagingCenter.Send(this, GoForwardKey);
}
public void Eval(string script)
{
MessagingCenter.Send(this, EvalKey, script);
}
}
}
iOSプロジェクトでの作業です。
新しく「MessageBasedWebViewRenderer.cs」を追加、以下の内容に書き換えます。
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;
using UIKit;
using Foundation;
[assembly: ExportRenderer(typeof(NakayokunaruHandsOn.MessageBasedWebView), typeof(NakayokunaruHandsOn.iOS.MessageBasedWebViewRenderer))]
namespace NakayokunaruHandsOn.iOS
{
public class MessageBasedWebViewRenderer : ViewRenderer<MessageBasedWebView, UIWebView>
{
protected override void OnElementChanged(ElementChangedEventArgs<MessageBasedWebView> e)
{
if (Control == null)
{
var nativeControl = new UIWebView();
nativeControl.LoadRequest(new NSUrlRequest(new NSUrl("http://ticktack.hatenablog.jp/entry/2016/06/11/124751")));
SetNativeControl(nativeControl);
}
if (e.OldElement != null)
{
MessagingCenter.Unsubscribe<MessageBasedWebView>(this, MessageBasedWebView.GoBackKey);
MessagingCenter.Unsubscribe<MessageBasedWebView>(this, MessageBasedWebView.GoForwardKey);
MessagingCenter.Unsubscribe<MessageBasedWebView, string>(this, MessageBasedWebView.EvalKey);
}
if (e.NewElement != null)
{
MessagingCenter.Subscribe<MessageBasedWebView>(
this,
MessageBasedWebView.GoBackKey,
_ => OnGoBackRequested(),
e.NewElement);
MessagingCenter.Subscribe<MessageBasedWebView>(
this,
MessageBasedWebView.GoForwardKey,
_ => OnGoForwardRequested(),
e.NewElement);
MessagingCenter.Subscribe<MessageBasedWebView, string>(
this,
MessageBasedWebView.EvalKey,
(sender, args) => OnEvalRequested(args),
e.NewElement);
}
base.OnElementChanged(e);
}
private void OnGoBackRequested()
{
if (Control.CanGoBack)
{
Control.GoBack();
}
}
private void OnGoForwardRequested()
{
if (Control.CanGoForward)
{
Control.GoForward();
}
}
private void OnEvalRequested(string script)
{
Control.EvaluateJavascript(script);
}
public override void LayoutSubviews()
{
base.LayoutSubviews();
Control.Frame = this.Bounds;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
MessagingCenter.Unsubscribe<MessageBasedWebView>(this, MessageBasedWebView.GoBackKey);
MessagingCenter.Unsubscribe<MessageBasedWebView>(this, MessageBasedWebView.GoForwardKey);
MessagingCenter.Unsubscribe<MessageBasedWebView, string>(this, MessageBasedWebView.EvalKey);
}
base.Dispose(disposing);
}
}
}
PCLプロジェクトでの作業です。
新しくContentPage「MessageBasedWebViewPage」を追加、以下の内容に書き換えます。
MessageBasedWebViewPage.xaml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:NakayokunaruHandsOn"
Title="MessageBasedWebView"
x:Class="NakayokunaruHandsOn.MessageBasedWebViewPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<local:MessageBasedWebView Grid.Row="0" x:Name="webView" />
<StackLayout Grid.Row="1" Orientation="Horizontal" HorizontalOptions="FillAndExpand">
<Button Text="戻る" Clicked="GoBackClicked"/>
<Button Text="進む" Clicked="GoForwardClicked"/>
</StackLayout>
<StackLayout Grid.Row="2" Orientation="Horizontal">
<Entry Text="location.reload()" x:Name="entry" HorizontalOptions="FillAndExpand"/>
<Button Text="Eval" Clicked="EvalClicked"/>
</StackLayout>
</Grid>
</ContentPage>
MessageBasedWebViewPage.xaml.cs
using Xamarin.Forms;
namespace NakayokunaruHandsOn
{
public partial class MessageBasedWebViewPage : ContentPage
{
public MessageBasedWebViewPage()
{
InitializeComponent();
}
void GoBackClicked(object sender, System.EventArgs e)
{
webView.GoBack();
}
void GoForwardClicked(object sender, System.EventArgs e)
{
webView.GoForward();
}
void EvalClicked(object sender, System.EventArgs e)
{
webView.Eval(entry.Text);
}
}
}
Appクラスのコンストラクタで以下の部分のコメントアウトを解除します。
toMessageBasedWebView.Clicked += async (s, e) =>
await rootPage.Navigation.PushAsync(new MessageBasedWebViewPage());
ここまで出来たらプロジェクトをビルドして実行してみましょう。 エントリーページで「MessageBasedWebView」を押して動作確認ページに移ります。 適当にWebページ内のリンクを開いた後、「進む」「戻る」ボタンが機能することを確認してください。 Entryコントロールの初期値として、JavascriptのEval確認用にページをリロードするコードが入っています。 「Eval」ボタンを押すとページが再読み込みすることを確認してください。