フューチャー技術ブログ

iOS ユニバーサルリンクをAzure BlobStorageでやってみる

この記事はグループ会社であるFutureOneの Qiita Organizationで公開された 記事 をクロスポストで公開しています。

はじめに

.NET MAUIでiOS向けアプリを開発中、WebアプリからiOSアプリを呼び出す必要がでてきたため、ユニバーサルリンクを実装しました。WebサーバーはAzure Blob Storageの静的サイトを利用します。

ユニバーサルリンクとは

ユニバーサルリンク(Universal Links)は、Webサイトへのリンクをクリックしたときに、そのリンクに対応するiOSアプリがインストールされていれば直接アプリを起動し、インストールされていなければWebサイトを表示するという機能です。

仕組み

  1. WebサーバーにApple App Site Association (以下: AASA)ファイルを置いておきます
  2. Apple CDNが定期的にWebサーバーのAASAファイルをクロールします
  3. iPhoneが、アプリインストール時にAppleCDNからAASAファイルを取得します
  4. Webサイト上で特定のURLを踏むとAASAファイルを使ってアプリを立ち上げる

簡単に図示すると以下のような感じになります。

WebサーバーにAzure Blob Storageを利用する

上記の通り、AppleCDNがアクセスできるWebサーバを準備し、UniversalLink用のAASAファイルを配備する必要があります。

そこで、安価に静的サイトを公開できるAzure BlobStorageの静的サイト機能を使いたいと思います。

Azure Storage構築

ストレージアカウントの作成

まずはストレージアカウントを作成します。

ストレージアカウントは基本的にデフォルトで大丈夫だと思いますが、必要に応じて値を変更します。基本情報のプライマリサービスは 「Azure Blob Storage または Azure Data Lake Storage Gen 2」 とします。

image.png

作ったものがこちらです。アカウントの種類が StorageV2 (汎用 v2) であることを確認します(上位のものでも大丈夫です)。

image.png

静的サイトを有効化

概要の下の方の機能タブから静的Webサイトを選択します。

image.png

ここで有効化します。プライマリエンドポイントが今回のUniversalLinkのドメインになります。myapp-universal-link-sample.z11.web.core.windows.net のような感じになると思います。

image.png

※ カスタムドメインを使用することも可能ですが、今回はスキップします。

静的サイトを有効化すると、$Web フォルダができます。

image.png

AASAファイルを配置

AASAファイルを準備します。

内容は JSON ですが、ファイルに拡張子はつけないで保存します。ファイル名は apple-app-site-association です。

今回はリンクURLによる制限はつけずにどんなURLでも呼び出し可能とします。

apple-app-site-association
{
"webcredentials": {
"apps": [
"アプリのID"
]
},
"applinks": {
"apps": [],
"details": [
{
"appIDs": [
"アプリのID"
],
"components": [
{
"/": "*"
}
]
}
]
}
}

$Web コンテナを開いてアップロードします。

右下のアップロード先のフォルダは .well-known にします。フォルダが存在しない場合でも、自動的に作成してくれます。

image.png

アップロードしたファイルを確認します。

階層が $Web/.well-known/apple-app-site-association となっていればOKです。

image.png

最後に ContentType を指定します。

ファイルをクリックするとプロパティが開くので編集します。application/json; charset=utf-8 にします。application/json は必須だと思います。 charset=utf-8はなくても大丈夫ですが、念の為に指定します。

image.png

$WebをPublic化

Apple CDNがアップロードしたAASAファイルを取得できるようにします。 コンテナを選択してアクセスレベルを変更します。 デフォルトだとプライベートになっていると思うので、コンテナしておきます。

image.png

AASAファイルを確認します。

先程のプライマリエンドポイントに .well-known/apple-app-site-association つけてURLバーに入れて(例: https://myapp-universal-link-sample/.well-known/apple-app-site-association)、AASAファイルが見えることを確認します。

image.png

Apple CDNを確認

アップロード後、しばらくするとAASAファイルをApple CDNが回収してくれます。

そのキャッシュは https://app-site-association.cdn-apple.com/a/v1/myapp-universal-link-sample.z11.web.core.windows.net で確認できます。

image.png

以上で環境構築は完了です。

Mauiアプリの設定

MauiプロジェクトのPlatformフォルダのIOSフォルダ直下に Entitlements.plist を作成します。内容は以下の通りです。

先程のプライマリエンドポイントのを記載します。「https://」は不要です。

Entitlements.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>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp-universal-link-sample.z11.web.core.windows.net</string>
</array>
</dict>
</plist>

Entitlements.plist を作りたくない人はcsprjで設定することもできます。

以下は、Apple universal linksより引用したコードです。

<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios' Or $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">

<!-- For debugging, use '?mode=developer' for debug to bypass apple's CDN cache -->
<CustomEntitlements
Condition="$(Configuration) == 'Debug'"
Include="com.apple.developer.associated-domains"
Type="StringArray"
Value="applinks:myapp-universal-link-sample.z11.web.core.windows.net?mode=developer" />

<!-- Non-debugging, use normal applinks:url value -->
<CustomEntitlements
Condition="$(Configuration) != 'Debug'"
Include="com.apple.developer.associated-domains"
Type="StringArray"
Value="applinks:myapp-universal-link-sample.z11.web.core.windows.net" />

</ItemGroup>

?mode=developer をつけておくとApple CDNをバイパスして、直接WebサーバからAASAファイルを取得するようになります。

次に、UniversalLinkで起動されたときのハンドラーを書きます。

Apple universal links - .NET MAUI | Microsoft Learn を参考にMauiProgram.cs を編集し、ユニバーサルリンク経由でアプリが起動・再開された際にURLを処理するためのライフサイクルイベントを登録します。

MauiProgram.cs
using Microsoft.Maui.LifecycleEvents;
using Microsoft.Extensions.Logging;

namespace MyNamespace;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureLifecycleEvents(lifecycle =>
{
#if IOS || MACCATALYST
lifecycle.AddiOS(ios =>
{
// Universal link delivered to FinishedLaunching after app launch.
ios.FinishedLaunching((app, data) => HandleAppLink(app.UserActivity));

// Universal link delivered to ContinueUserActivity when the app is running or suspended.
ios.ContinueUserActivity((app, userActivity, handler) => HandleAppLink(userActivity));

// Only required if using Scenes for multi-window support.
if (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsMacCatalystVersionAtLeast(13))
{
// Universal link delivered to SceneWillConnect after app launch
ios.SceneWillConnect((scene, sceneSession, sceneConnectionOptions)
=> HandleAppLink(sceneConnectionOptions.UserActivities.ToArray()
.FirstOrDefault(a => a.ActivityType == Foundation.NSUserActivityType.BrowsingWeb)));

// Universal link delivered to SceneContinueUserActivity when the app is running or suspended
ios.SceneContinueUserActivity((scene, userActivity) => HandleAppLink(userActivity));
}
});
#endif
});

#if DEBUG
builder.Logging.AddDebug();
#endif

return builder.Build();
}

#if IOS || MACCATALYST
static bool HandleAppLink(Foundation.NSUserActivity? userActivity)
{
if (userActivity is not null && userActivity.ActivityType == Foundation.NSUserActivityType.BrowsingWeb && userActivity.WebPageUrl is not null)
{
HandleAppLink(userActivity.WebPageUrl.ToString());
return true;
}
return false;
}
#endif

static void HandleAppLink(string url)
{
if (Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out var uri))
App.Current?.SendOnAppLinkRequestReceived(uri);
}
}

Appクラスの OnAppLinkRequestReceived を呼んでくれるのでOverrideすることで受け取ったURLのハンドルができます。
クエリパラメータもらうときなどはここで処理するとよいと思います。

protected override void OnAppLinkRequestReceived(Uri uri)
{
//uriの処理が必要であれば
}

Mauiアプリの修正は以上です。

Apple DeveloperアカウントのアプリIDに、関連ドメインの使用を許可

UniversalLinkを使うにはApple上のアプリに使用を許可する必要があります。

手順は以下の通りです(参考: Apple universal links)。

  1. Web ブラウザでApple Developer アカウントにログインし、 「証明書、ID、およびプロファイル」ページに移動します
  2. 「証明書、識別子、プロファイル」ページで、「識別子」タブを選択します
  3. 「識別子」ページで、アプリに対応するアプリ ID を選択します
  4. 「App ID 構成の編集」ページで、「関連ドメイン」機能を有効にし、「保存」ボタンを選択しますimage.png
  5. profileに反映させます。Editから先程設定したApp IDを選択します
    Enabled CapacitiesにAssociated DomainがあればOKですimage.png
  6. 最後にVisual Studioのプロファイルを更新します

アプリをリリースする

あとはアプリをリリースするだけです。デバッグでも大丈夫です。

今回の場合のユニバーサルリンクのURLはブロブストレージのプライマリエンドポイント(例: https://myapp-universal-link-sample.z11.web.core.windows.net/)になります。これをWebサイトに埋め込んでおきます。

アプリがインストールされている状態であれば、iPhoneのメモ帳でも確認ができます。リンク用のURLを打ち込んで長押しするとアプリを開くという選択肢がでてきます。

image.png

最後に

Azure Blob Storageの静的サイト機能で簡単にUniversalLink用のWebサーバを立ち上げられました。

Azure環境がある場合は選択肢に入れてもいいかなと思います。