Xamarin.Forms でガワネイティブアプリを作るときのテンプレートプロジェクトを作る２

Advent Calendar 最終日が空いていたので、また [リクエストもあった](https://twitter.com/ytabuchi/status/1206422081694388224) ので埋めましょう（Xamarin のカレンダーが半分Webネタで良いのか？は置いといて）。

というわけで、

* [Xamarin.Forms でガワネイティブアプリを作るときのテンプレートプロジェクトを作る１ - Qiita](https://qiita.com/amay077/items/60dc567f0a7bca3ccd31)

の続きです。

## 目次

1. 【[第1回](https://qiita.com/amay077/items/60dc567f0a7bca3ccd31)】日本語入力時の画面高さの調整
2. 【第1回】ステータスバー、あるいは SafeArea(ノッチ部)の色
3. **【今回】スプラッシュスクリーンおよび初回読み込み時の対応**
4. **【今回】アプリに必要な権限の許可を要求する**
5. 【次回以降予定】アプリ情報の Web 側への引き渡し
6. 【次回以降予定】``<input type="xxx">`` への対応
7. 【次回以降予定】Back ボタンハンドリングの Web 側への移譲

## 3. スプラッシュスクリーンおよび初回読み込み時の対応

実体がWebアプリであるため、起動してからアプリが使用可能になるまでに時間がかかるのは、ガワネイティブの弱点の一つです。
これはなんとかごまかしてユーザーに不快感を与えないようにしたいです。

ガワネイティブの起動にかかるプロセスは大きくわけて２つです。

1. ネイティブアプリとしての起動から最初の画面が表示されるまで
2. 最初の画面が表示されてから Web ページの読み込みが完了するまで

1 は通常のアプリでも必要なプロセス、2 はガワネイティブ特有の要件です。

まずは 1 を対応します。

### Android の場合

* [Splash Screen - Xamarin | Microsoft Docs](https://docs.microsoft.com/en-us/xamarin/android/user-interface/splash-screen)

こちらを参考にします。

Android 側プロジェクトの ``Resources/drawable/splash.xml`` を追加して、次のように記述します。

**splash.axml**

```xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:opacity="opaque"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <item
        android:drawable="@color/colorPrimaryDark"/>
</layer-list>
```

次に、``Resources/values/colors.xml`` の ``colorPrimaryDark`` の色を Web アプリのテーマ色に合わせます。
この例では ``#66BB6A`` とします。

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="launcher_background">#FFFFFF</color>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#66BB6A</color>
    <color name="colorAccent">#FF4081</color>
</resources>
```

前述のリンクでは、スプラッシュスクリーン用の Theme と Activity を作るよう書かれていますが、ガワネイティブの場合少しサボることができます。
``Resources/values/styles.xml`` を開き、``MainTheme.Base`` に ``android:windowBackground`` を追加します。

**styles.xml**

```xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="MainTheme" parent="MainTheme.Base">
    </style>
    <!-- Base theme applied no matter what API -->
    <style name="MainTheme.Base" parent="Theme.AppCompat.Light.DarkActionBar">
        <item name="android:windowBackground">@drawable/splash</item>
        <!--If you are using revision 22.1 please use just windowNoTitle. Without android:-->
        <item name="windowNoTitle">true</item>
        <!--We will be using the toolbar so no need to show ActionBar-->
        <item name="windowActionBar">false</item>
        <!-- Set theme colors from https://aka.ms/material-colors -->
        <!-- colorPrimary is used for the default action bar background -->
        <item name="colorPrimary">#2196F3</item>
        <!-- colorPrimaryDark is used for the status bar -->
        <item name="colorPrimaryDark">#66BB6A</item>
        <!-- colorAccent is used as the default value for colorControlActivated
         which is used to tint widgets -->
        <item name="colorAccent">#FF4081</item>
        <!-- You can also set colorControlNormal, colorControlActivated
         colorControlHighlight and colorSwitchThumbNormal. -->
        <item name="windowActionModeOverlay">true</item>
        <item name="android:datePickerDialogTheme">@style/AppCompatDialogStyle</item>
    </style>
    <style name="AppCompatDialogStyle" parent="Theme.AppCompat.Light.Dialog">
        <item name="colorAccent">#FF4081</item>
    </style>
</resources>
```

これでアプリ起動時に緑のスプラッシュスクリーンが表示されるようになります。

### iOS の場合

iOS プロジェクトには、最初から ``LaunchScreen.storyboard`` がスプラッシュスクリーンとして使用されるよう設定されていますが、色が青になっているので、これを変更します。

``Resources/LaunchScreen.storyboard`` を開いて、``Background`` の色を ``#66BB6A`` に変更しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/7fc9018a-ddbd-eb51-7144-ad5dafee9554.png)

次に 「2. 最初の画面が表示されてから Web ページの読み込みが完了するまで」の対応です。
こちらはベタに、 **WebView の読み込みが完了するまで、WebView を隠しておく** ことにします。
これは Xamarin.Forms の共通プロジェクトで対応できます。

``MainPage.xaml`` を次のように修正します。

**MainPage.xaml**

```xml
<?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:d="http://xamarin.com/schemas/2014/forms/design"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    xmlns:local="clr-namespace:GawaNativeGettingStarted;assembly=GawaNativeGettingStarted"
    xmlns:effect="clr-namespace:GawaNativeGettingStarted"
    x:Class="GawaNativeGettingStarted.MainPage">
    <StackLayout
        Orientation="Vertical"
        VerticalOptions="FillAndExpand"
        BackgroundColor="#66BB6A">
        <StackLayout.Effects>
            <effect:SafeAreaPaddingEffect />
        </StackLayout.Effects>
        <WebView
            x:Name="webView"
            IsVisible="false"
            HorizontalOptions="FillAndExpand"
            VerticalOptions="FillAndExpand"
            Source="https://8440f155.ngrok.io"/>
        <FlexLayout
            x:Name="progressView"
            Direction="Column"
            JustifyContent="Center"
            VerticalOptions="FillAndExpand">
            <ActivityIndicator
                IsRunning="True"
                Color="White"
                VerticalOptions="Center" />
            <Label
                FontSize="Large"
                TextColor="White"
                HorizontalTextAlignment="Center"
                Text="Launching..." />
        </FlexLayout>
    </StackLayout>
</ContentPage>
```

そしてコードビハインドの ``MainPage.xaml.cs`` に次のように記述します。

**MainPage.xaml.cs**

```csharp
// 略
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        webView.Navigated += (s, e) =>
        {
            progressView.IsVisible = false;
            webView.IsVisible = true;
        };
    }
}
```
WebView の読み込みが完了すると ``Navigated`` イベントが発生するので、それが発生するまでは、WebView は非表示、代わりにプログレス的な View を表示しておきます。
Web アプリ側が真に SPA(Single Page Application) であるなら、``Navigated`` イベントは一度しか発生しないはずですね。

ここまでの成果を実行すると、次のような見た目になります。

![Untitled7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/26da9377-11dd-bfa1-2a8b-1980590bc777.gif)

（Xamarin.Forms アプリとしての起動遅っそ。そして iOS シミュレータ早っや！）

## 4. アプリに必要な権限の許可を要求する

例えば Web アプリ側の JavaScript で Geolocation API を使用する場合も、ガワの方でその許可を得ておく必要があります(Chrome などのブラウザアプリが実装していることをやらなければならない)。

さて、Xamarin.Forms アプリとして作成したプロジェクトには [Xamarin Essentials](https://docs.microsoft.com/en-us/xamarin/essentials/) が標準で組み込まれており、それらの機能を使う際には、必要な権限の許可が Xamarin Essentials から要求されるようです。

しかし、JavaScript 側で呼び出された機能に必要な許可要求は、自力で行う必要があります [^1]。
そのために、

* [jamesmontemagno/PermissionsPlugin: Check and Request Permissions Plugin for Xamarin and Windows](https://github.com/jamesmontemagno/PermissionsPlugin)

を使用します。

[^1]: Xamarin Essentials の GitHub で master ブランチの [``Permissions.cs``](https://github.com/xamarin/Essentials/blob/b1f8dcb39d12d16ede41061ae9899c2ba5f26cfe/Xamarin.Essentials/Permissions/Permissions.netstandard.cs) を見ると、 ``Permissions`` クラスが ``public`` になっているので、今後のバージョンアップで、権限機能が個別に使用できるようになるのかも知れません。

バージョンが Stable で最新の 3.0.0.12、この nuget パッケージを共通、Android、iOS のすべてのプロジェクトにそれぞれ追加します。

Android プロジェクトには、 [Setup](https://github.com/jamesmontemagno/PermissionsPlugin#setup) に書かれているように、 ``MainActivity.cs`` に数行のコードを足す必要があります。

**MainActivity.cs**

```csharp
＜省略＞

public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        ＜省略＞

        Xamarin.Forms.Application.Current.On<Xamarin.Forms.PlatformConfiguration.Android>()
            .UseWindowSoftInputModeAdjust(WindowSoftInputModeAdjust.Resize);
        
        // ココ！
        Plugin.CurrentActivity.CrossCurrentActivity.Current.Init(this, savedInstanceState);
    }

    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
    {
        // ココ！
        PermissionsImplementation.Current.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}
```

iOS の方はここでは特にする事はありません。

準備が整ったところで、実際に必要な権限の許可を求める処理を書いてみます。

例として

* 位置情報の取得
* カメラの使用

の権限を求めてみます。

### Android/iOS プロジェクトの設定

**位置情報の取得**

[Xamarin.Essentials: Geolocation](https://docs.microsoft.com/en-us/xamarin/essentials/geolocation?tabs=android) に書かれているように、

Android プロジェクトでは、 ``AssemblyInfo.cs`` と ``AndroidManifest.xml`` に数行ずつ追加します。
iOS プロジェクト側は、``Info.plist`` を編集します。

**カメラの使用**

Android 側は、``AndroidManifest.xml`` に、

```xml
<uses-permission android:name="android.permission.CAMERA" />
```

を追加します。

iOS プロジェクトは、``Info.plist`` を開き、新しいエントリ「Privacy - Camera Usage Description」を追加し、Value に適当な説明文を入力します。

その他の権限に必要な設定は、

Android は、

* [システム パーミッション / Android Developers](https://developer.android.com/guide/topics/security/permissions.html#normal-dangerous)

iOS では、

* [iOS Security and Privacy Features - Xamarin | Microsoft Docs](https://docs.microsoft.com/en-us/xamarin/ios/app-fundamentals/security-privacy?tabs=windows)

が詳しいです。

### 共通プロジェクトでの記述

では、いよいよ、許可を求めるコードを書いていきます。

共通プロジェクトの ``MainPage.xaml.cs`` に次のように記述します。

**MainPage.xaml.cs**

```csharp
public partial class MainPage : ContentPage
{
    ＜省略＞

    async protected override void OnAppearing()
    {
        base.OnAppearing();

        // 許可を求める権限群
        var permissions = new []
        {
            Permission.Location,
            Permission.Camera,
        };

        // 許可を得ていない権限群
        var notPermitteds = new List<Permission>();
        foreach (var perm in permissions)
        {
            var status = await CrossPermissions.Current.CheckPermissionStatusAsync(perm);
            if (status != PermissionStatus.Granted)
            {
                notPermitteds.Add(perm);
            }
        }

        // 許可を求めるダイアログボックスを出す
        var statusMap = await CrossPermissions.Current.RequestPermissionsAsync(notPermitteds.ToArray());
        var permitted = statusMap.Values.All(x => x == PermissionStatus.Granted);

        if (!permitted)
        {
            await DisplayAlert("許可求む", "すべての権限が許可されていないので、一部の機能が使用できないかもしれません。", "閉じる");
        }

        // 許可されたら Web アプリ起動
        webView.Source = "https://5ce07f72.ngrok.io";
    }
}
```

これは、アプリの起動時に、必要な権限群 ``permissions`` について、

1. ``CheckPermissionStatusAsync`` で許可が得られているかを取得
2. 許可が得られていない権限群について ``RequestPermissionsAsync`` で許可を求めるダイアログボックスを表示
3. ユーザーが許可しなかったら、注意喚起するアラートを表示
4. WebView に URL を設定し、Webアプリを起動

という流れです。

このコードを動かすと、次の図のようになります。

![Untitled7.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8227/ad0c3234-bd31-ff9d-fbfb-69d2c15e5b5e.gif)

ハマりポイントとしては、iOS で ``pinfo.list`` へ追加し忘れると ``RequestPermissionsAsync`` を呼んだ瞬間にアプリが落ちる、があります。

権限の取得は最小限、且つユーザーに納得感の強いタイミングで行うべきなので、起動時にまとめて行うのは、あまりお行儀の良いアプリとは言えません。

次回以降で解説する内容は、ネイティブアプリの情報を Webアプリ に引き渡す方法や、Webアプリのアクションをネイティブ側に処理させる方法などです。
これらを駆使して、必要なタイミングで必要な許可を求めるコードを書くこともできます。

2020年も「Xamarin.Forms＋ガワネイティブ」推しでゆくべく、この連載は続けていきたいと思います。
では 2020年 にお会いしましょう。