noxi雑記

.NET、Angularまわりの小ネタブログ

Xamarin.Formsで各プロジェクト毎に環境に応じた設定ファイルを作成する

Qiitaからの移植

はじめに

Xamarin.Formsを開発しているとプロジェクト構成は、ViewやModelを記述するコアのプロジェクトと、iOS/Androidの各プラットフォームプロジェクトの大きく2つに分かれます。アプリの動作に必要な設定情報をどう持たせるかについて色々議論はあるかと思いますが、先日開発したプロジェクトでの実装を紹介します。 よくありがちな、認証の設定値をプログラム内にべた書きし #if DEBUG などのディレクティブを使用することで環境毎に切り替えるを避けることを重視しています。またソース管理上に秘匿すべき設定が含まれないようにします。

TL;DR

Modelプロジェクト側に設定から取得したい情報をまとめたインターフェースを作成し、各プラットフォームプロジェクト側でそれぞれ実際の実装クラスを作成します。これをXamarin.Formsの DependencyService やPrismのDIで取得します。

環境

実装

実装の方針は先に記述した通り、ソースコードべた書き& #if ディレクティブを使用して環境を切り替えるような実装を避け、ソース管理外の設定ファイルに設定値を記述してそれを参照する様なコードを書くことです。筆者はこれを実現するためにiOSでは専用のplistファイル、Androidではstringリソースを使用しました。 今回はユーザーログインに利用するAzure ActiveDirectory(AAD)の認証を例とします。

プロジェクト構造

今回はXamarin.Formsのプロジェクトですので、プロジェクト(ソリューション)構造は次のようになっています。

(Root)
  ├ Sample.Core
  ├ Sample.Android
  └ Sample.iOS

Sample.CoreはViewやViewModel、Modelが含まれる、各プラットフォームプロジェクトから参照されるプロジェクトです。Sample.Android、Sample.iOSはそれぞれのプラットフォーム向けの実装を含むXamarinアプリケーションプロジェクトです。

共通インターフェース作成

AAD認証をC#から利用するのに便利な方法は Azure Active Directory Authentication Library (ADAL: Microsoft.IdentityModel.Clients.ActiveDirectory) を使用することです。Xamarin.FormsでADALをどう使うのかはさておき、以下の値が必要になります。

説明
Authority 認証トークンを発行するURL。
Resource 使用するリソースのID。
Client ID クライアントアプリのID。
Redirect URL 認証後のリダイレクト先URL。

これらの設定値を取得するためのインターフェースをSample.Coreプロジェクト内に用意します。

public interface IAdalConfig
{
    string Authority { get; }
    string ResouceId { get; }
    string ClientId { get; }
    string RedirectUrl { get; }
}

iOSプロジェクトの実装

iOSでは設定値をplistに持たせ、C#からはそのplistから取得した値を保持するのみとします。

plistファイルの作成

Sample.iOSプロジェクト直下に AdalConfig.plistAdalConfig.Debug.plistAdalConfig.Release.plist の3ファイルを用意します。DebugはDebugビルド時に、ReleaseはReleaseやAd-Hoc、AppStoreビルド時に参照する設定ファイルです。内容は全て同じです。

<?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>Authority</key>
  <string></string>
  <key>ResouceId</key>
  <string></string>
  <key>ClientId</key>
  <string></string>
  <key>RedirectUrl</key>
  <string></string>
</dict>
</plist>

プロジェクトファイルの修正

plistファイルをただ追加しただけでは全てのファイルがビルド対象に含まれてしまうため、プロジェクトファイル(csprojファイル)を直接編集して、ビルド環境に応じてビルド対象に含めるファイルを変更します。

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <ItemGroup>
    <!-- Debug/Releaseが付いていないplistファイルはコンパイル時に無視されます -->
    <None Include="AdalConfig.plist" />
  </ItemGroup>
  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
    <!-- Debugビルド時のみAdalConfig.Debug.plistをコンテンツとして扱います -->
    <Content Include="AdalConfig.Debug.plist">
      <DependentUpon>AdalConfig.plist</DependentUpon>
    </Content>
  </ItemGroup>
  <ItemGroup Condition=" '$(Configuration)' == 'Release' OR '$(Configuration)' == 'Ad-Hoc' OR '$(Configuration)' == 'AppStore' ">
    <!-- Release/Ad-Hoc/AppStoreビルド時のみAdalConfig.Release.plistをコンテンツとして扱います -->
    <Content Include="AdalConfig.Release.plist">
      <DependentUpon>AdalConfig.plist</DependentUpon>
    </Content>
  </ItemGroup>
  ...
</Project>

plistを読み込むクラスの作成

Xamarin.FormsのDependencyに登録するためのplistを読み込んでマッピングするクラスを作成します。

[assembly:Dependency(typeof(AdalConfig))]

public class AdalConfig : IAdalConfig
{
    public string Authority { get; }
    public string ResouceId { get; }
    public string ClientId { get; }
    public string RedirectUrl { get; }

    public AdalConfig()
    {
#if DEBUG
        var path = NSBundle.MainBundle.PathForResource("AdalConfig.Debug", "plist");
#else
        var path = NSBundle.MainBundle.PathForResource("AdalConfig.Release", "plist");
#endif
        var config = NSDictionary.FromFile(path);
        Authority = config[nameof(Authority)].ToString();
        ResourceId = config[nameof(ResourceId)].ToString();
        ClientId = config[nameof(ClientId)].ToString();
        RedirectUrl = config[nameof(RedirectUrl)].ToString();
    }
}

plistファイルを取得する際のファイルパス解決だけはifディレクティブを使用しています。ビルド時にコンテンツファイルの名前を変更する方法が分からなかったためいたしかたがなく、、、

ignoreの追加

最後に、Debug/Release向けの設定ファイルをソース管理上から追い出すためにignoreを追加します。筆者はSample.iOSプロジェクト直下に配置しました。

AdalConfig.*.plist 

Androidプロジェクトの実装

Androidでは設定値をstringリソース内に持たせ、C#からはそのリソースから取得した値を保持するのみとします。

stringリソースファイルの作成

Sample.AndroidプロジェクトのResouces/valuesに strings_adal_config.xmlstrings_adal_config_debug.xmlstrings_adal_config_release.xml の3ファイルを用意します。こちらもiOS同様、debugはDebugビルド時に、releaseはReleaseビルド時に参照する設定ファイルです。内容は全て同じです。

<?xml version="1.0" encoding="utf-8"?>
<resources>

  <string name="Authority"></string>
  <string name="ResourceId"></string>
  <string name="ClientId"></string>
  <string name="RedirectUrl"></string>

</resources>

プロジェクトファイルの修正

stringリソースファイルをただ追加しただけでは全てのファイルがビルド対象に含まれてしまうため、プロジェクトファイル(csprojファイル)を直接編集して、ビルド環境に応じてビルド対象に含めるファイルを変更します。

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  ...
  <ItemGroup>
    <!-- Debug/Releaseが付いていないplistファイルはコンパイル時に無視されます -->
    <None Include="Resources\values\strings_adal_config.xml" />
  </ItemGroup>
  <ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
    <!-- Debugビルド時のみAdalConfig.Debug.plistをコンテンツとして扱います -->
    <AndroidResource Include="Resources\values\strings_adal_config_debug.xml">
      <DependentUpon>Resources\values\strings_adal_config.xml</DependentUpon>
    </AndroidResource>
  </ItemGroup>
  <ItemGroup Condition=" '$(Configuration)' == 'Release' ">
    <!-- Release/Ad-Hoc/AppStoreビルド時のみAdalConfig.Release.plistをコンテンツとして扱います -->
    <AndroidResource Include="Resources\values\strings_adal_config_release.xml">
      <DependentUpon>Resources\values\strings_adal_config.xml</DependentUpon>
    </AndroidResource>
  </ItemGroup>
  ...
</Project>

DependentUpon を使用すると該当の項目が、例えばWeb.configのようにディレクトリのような親子関係として表示されるのですが、AndroidResourceではそれが動作しませんでした。

stringリソースを読み込むクラスの作成

Xamarin.FormsのDependencyに登録するための、stringリソースを読み込んでマッピングするクラスを作成します。

[assembly:Dependency(typeof(AdalConfig))]

public class AdalConfig : IAdalConfig
{
    private readonly Lazy<string> _authority = new Lazy<string>(() => Forms.Context.GetString(Resource.String.Authority));
    private readonly Lazy<string> _resourceId = new Lazy<string>(() => Forms.Context.GetString(Resource.String.ResourceId));
    private readonly Lazy<string> _clientId = new Lazy<string>(() => Forms.Context.GetString(Resource.String.ClientId));
    private readonly Lazy<string> _redirectUri = new Lazy<string>(() => Forms.Context.GetString(Resource.String.RedirectUri));

    public string Authority => _authority.Value;
    public string ResourceId => _resourceId.Value;
    public string ClientId => _clientId.Value;
    public string RedirectUri => _redirectUri.Value;
}

ignoreの追加

最後に、Debug/Release向けの設定ファイルをソース管理上から追い出すためにignoreを追加します。筆者はSample.Androidプロジェクト直下に配置しました。

**/*_debug.xml
**/*_release.xml

終わりに

ツイッターでDesigner.csをどうするか、という話が出ていたので、Designer.csの管理とは関係ありませんが似たような話だったので記事にしてみました。Xamarin.Formsは楽しいけどとても面倒くさいですね。他にも何かあれば上げていこうと思います。