Apple 通用链接

通常需要连接网站和移动应用,以便网站上的链接启动移动应用并在移动应用中显示内容。 应用链接(也称为深层链接)是一种技术,它使移动设备能够响应 URL 并在 URL 所表示的移动应用中启动内容。

在 Apple 平台上,深层链接称为通用链接。 当用户点击通用链接时,系统会直接将链接重定向到应用,而无需通过 Safari 或网站进行路由。 这些链接可以基于自定义方案,例如 myappname://,也可以使用 HTTP 或 HTTPS 方案。 例如,单击食谱网站上的链接将打开与该网站关联的移动应用,然后向用户显示特定的食谱。 未安装应用的用户将被带到网站上的内容。 本文重点介绍使用 HTTPS 方案的通用链接。

.NET MAUI iOS 应用支持通用链接。 这需要在域上托管数字资产链接 JSON 文件,该文件描述与应用的关系。 这使 Apple 能够验证尝试处理 URL 的应用是否拥有 URL 域的所有权,以防止恶意应用截获应用链接。

在 .NET MAUI iOS 或 Mac Catalyst 应用中处理 Apple 通用链接的过程如下所示:

有关详细信息,请在 developer.apple.com 参阅允许应用和网站链接到你的内容。 有关为应用定义自定义 URL 方案的信息,请在 developer.apple.com 参阅为应用定义自定义 URL 方案

创建和托管关联的域文件

若要将网站与应用关联,需要在网站上托管关联的域文件。 关联的域文件是一个 JSON 文件,必须托管在你的域的以下位置:https://domain.name/.well-known/apple-app-site-association

以下 JSON 显示了典型关联域文件的内容:

{
    "activitycontinuation": {
        "apps": [ "85HMA3YHJX.com.companyname.myrecipeapp" ]
    },
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "85HMA3YHJX.com.companyname.myrecipeapp",
                "paths": [ "*", "/*" ]
            }
        ]
    }
}

appsappID 键应为可在网站上使用的应用指定应用标识符。 这些键的值由应用标识符前缀和捆绑标识符组成。

重要

必须使用具有有效证书和无重定向的 https 托管关联的域文件。

有关详细信息,请在 developer.apple.com 参阅支持关联域

向应用添加关联的域权利

在域上托管关联的域文件后,需要向应用添加关联的域权利。 当用户安装你的应用时,iOS 会尝试下载关联的域文件并验证权利中的域。

关联的域权利会指定与应用关联的域列表。 此权利应添加到你的应用中的 Entitlements.plist 文件。 有关在 iOS 上添加权利的详细信息,请参阅权利。 有关在 Mac Catalyst 上添加权利的详细信息,请参阅权利

权利是使用 StringArray 类型的 com.apple.developer.associated-domains 密钥定义的:

<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:recipe-app.com</string>
</array>

有关此权利的详细信息,请在 developer.apple.com 参阅关联的域权利

或者,可以修改项目文件 (.csproj),以在 <ItemGroup> 元素中添加权利:

<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:recipe-app.com?mode=developer" />

    <!-- Non-debugging, use normal applinks:url value -->
    <CustomEntitlements
        Condition="$(Configuration) != 'Debug'"
        Include="com.apple.developer.associated-domains"
        Type="StringArray"
        Value="applinks:recipe-app.com" />

</ItemGroup>

在此示例中,为你的域将 applinks:recipe-app.com 替换为正确的值。 确保仅包含所需的子域和顶级域。 不要包含路径和查询组件或末尾的斜杠 (/)。

注意

在 iOS 14+ 和 macOS 11+ 中,应用不再直接向你的 Web 服务器发送 apple-app-site-association 文件的请求。 它们会转而将请求发送到专用于关联域的 Apple 托管内容分发网络 (CDN)。

将关联的域功能添加到应用 ID

将关联的域权利添加到应用后,需要将关联的域功能添加到 Apple 开发者帐户中应用的应用 ID 中。 这是必需的,因为应用中定义的任何权利也需要作为功能添加到 Apple 开发者帐户中应用的应用 ID 中。

将关联的域功能添加到应用 ID:

  1. 在 Web 浏览器中,登录到 Apple 开发者帐户并导航到“证书、ID 和配置文件”页面

  2. 在“证书、标识符和配置文件”页上,选择“标识符”选项卡

  3. 在“标识符”页上,选择与应用相对应的应用 ID

  4. 在“编辑应用 ID 配置”页上,启用“关联域”功能,然后选择“保存”按钮

    Screenshot of enabling the associated domains capability in the Apple Developer Portal.

  5. 在“修改应用功能”对话框中,选择“确认”按钮

更新应用的应用 ID 后,需要生成并下载更新后的预置描述文件。

注意

如果以后从应用中删除关联域权利,则需要在 Apple 开发者帐户中更新应用 ID 的配置。

当用户激活通用链接时,iOS 和 Mac Catalyst 会启动应用并将其发送到 NSUserActivity 对象。 可以查询此对象以确定你的应用的启动方式,并确定要执行的操作。 这应在 FinishedLaunchingContinueUserActivity 生命周期委托中执行。 当应用启动时,会调用 FinishedLaunching 委托,当应用正在运行或挂起时,会调用 ContinueUserActivity 委托。 有关生命周期委托的详细信息,请参阅平台生命周期事件

要响应正在调用的 iOS 生命周期委托,请在 MauiProgram 类的 CreateMauiapp 方法中对 MauiAppBuilder 对象调用 ConfigureLifecycleEvents 方法。 然后,在 ILifecycleBuilder 对象上,调用 AddiOS 方法并指定为所需委托注册处理程序的 Action

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);
    }
}

当 iOS 打开你的应用作为通用链接的结果时,NSUserActivity 对象将具有 ActivityType 属性,其值为 BrowsingWeb。 活动对象的 WebPageUrl 属性将包含用户想要访问的 URL。 可以使用 SendOnAppLinkRequestReceived 方法将该 URL 传递给你的 App 类。

注意

如果你不在你的应用中使用场景来支持多窗口,则可以省略场景方法的生命周期处理程序。

在你的 App 类中,替代用于接收和处理 URL 的 OnAppLinkRequestReceived 方法:

namespace MyNamespace;

public partial class App : Application
{
    public App()
    {
        InitializeComponent();

        MainPage = new AppShell();
    }

    protected override async void OnAppLinkRequestReceived(Uri uri)
    {
        base.OnAppLinkRequestReceived(uri);

        // Show an alert to test that the app link was received.
        await Dispatcher.DispatchAsync(async () =>
        {
            await Windows[0].Page!.DisplayAlert("App link received", uri.ToString(), "OK");
        });

        Console.WriteLine("App link: " + uri.ToString());
    }
}

在上面的示例中,OnAppLinkRequestReceived 替代会显示应用链接 URL。 实际上,应用链接应将用户直接转到 URL 表示的内容,而不会出现任何提示、登录或其他中断。 因此,OnAppLinkRequestReceived 替代是从中调用到 URL 所表示内容的导航的位置。

警告

通用链接为应用提供了潜在的攻击途径,因此请确保验证所有 URL 参数并丢弃任何格式不正确的 URL。

有关详细信息,请在 developer.apple.com 参阅在应用中支持通用链接

重要

在 iOS 上,应在设备上而不是模拟器上测试通用链接。

若要测试通用链接,请将链接粘贴到“备忘录”应用中,并长按它(在 iOS 上)或按 control 并单击它(在 macOS 上),以发现跟随该链接的选项。 如果已正确配置通用链接,则会出现在应用和 Safari 中打开的选项。 你的选择将设置跟随此域中的通用链接时,你的设备上的默认行为。 若要更改此默认选项,请重复这些步骤并做出其他选择。

注意

在 Safari 中输入 URL 始终不会打开应用。 相反,Safari 将接受此操作作为直接导航。 如果用户直接导航到你的域后位于你的域中,那么你的网站将显示一个横幅以打开你的应用。

在 iOS 上,你可以在开发人员设置中使用关联的域诊断测试来测试通用链接:

  1. 在“设置”中启用开发人员模式。 有关详细信息,请在 developer.apple.com 参阅在设备上启用开发人员模式
  2. 在“设置 > 开发人员”中,滚动到“通用链接”,然后启用“关联的域开发”。
  3. 打开“诊断”并在 URL 中键入。 然后,你将收到有关链接是否对已安装的应用有效的反馈。

通常,无效的通用链接是 applinks 配置错误的结果。

如需故障排除建议,请在 developer.apple.com 参阅调试通用链接