Xamarin.Formsで各プロジェクト毎に環境に応じた設定ファイルを作成する
はじめに
Xamarin.Formsを開発しているとプロジェクト構成は、ViewやModelを記述するコアのプロジェクトと、iOS/Androidの各プラットフォームプロジェクトの大きく2つに分かれます。アプリの動作に必要な設定情報をどう持たせるかについて色々議論はあるかと思いますが、先日開発したプロジェクトでの実装を紹介します。
よくありがちな、認証の設定値をプログラム内にべた書きし #if DEBUG
などのディレクティブを使用することで環境毎に切り替えるを避けることを重視しています。またソース管理上に秘匿すべき設定が含まれないようにします。
TL;DR
Modelプロジェクト側に設定から取得したい情報をまとめたインターフェースを作成し、各プラットフォームプロジェクト側でそれぞれ実際の実装クラスを作成します。これをXamarin.Formsの DependencyService
やPrismのDIで取得します。
環境
- Windows 10
- Visual Studio 2017 15.4
- Xamarin.Forms 2.4.x
実装
実装の方針は先に記述した通り、ソースコードべた書き& #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.plist
、 AdalConfig.Debug.plist
、 AdalConfig.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.xml
、 strings_adal_config_debug.xml
、 strings_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は楽しいけどとても面倒くさいですね。他にも何かあれば上げていこうと思います。