PAYATHON 2026

如何使用 Xamarin.Forms Shell 在 iOS App 内加载页面?

支付阿杰

结论

FormsApplicationDelegate 是 Xamarin.Forms 对 UIApplicationDelegate 的封装,iOS 仍然有 root view controller。问题通常出在以下几个方面:

  • 在 base.FinishedLaunching(...) 完成前读取了 RootViewController;
  • 使用 UIApplication.SharedApplication.KeyWindow,但当前没有可用的 KeyWindow;
  • 额外创建了 UIWindow,覆盖或绕过 Xamarin.Forms 创建的窗口;
  • 直接从根控制器展示页面,没有使用当前最上层的控制器;
  • 支付 SDK 要求跳转浏览器或外部支付 App,无法通过导航代码强制改成内嵌页面。

不要在 AppDelegate 中创建第二个空白 UIWindow。直接使用 Xamarin.Forms 已经创建的 Window.RootViewController,找到当前可见的 UIViewController,然后在主线程调用 PresentViewController。

如果支付 SDK 明确要求传入 UINavigationController,可以从当前控制器层级中查找。找不到时,也可以用新的 UINavigationController 包装支付控制器,再以 modal 方式展示。

为什么 KeyWindow.RootViewController 可能是 null

下面的继承关系没有问题:

public partial class AppDelegate :
    Xamarin.Forms.Platform.iOS.FormsApplicationDelegate

FormsApplicationDelegate 仍然参与标准的 iOS application delegate 生命周期。Xamarin.Forms 主要在执行 base.FinishedLaunching(...) 时初始化原生窗口和根控制器。

例如:

LoadApplication(new App());
return base.FinishedLaunching(app, options);

如果在 return base.FinishedLaunching(...) 之前查询 root view controller,它可能还没有准备好。

另外,在多窗口生命周期下,UIApplication.SharedApplication.KeyWindow 并不可靠。即使项目没有主动使用 Scene,也不应把它作为唯一入口。

保留 Xamarin.Forms 创建的窗口

AppDelegate 可以写成:

[Register("AppDelegate")]
public partial class AppDelegate :
    global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
    public override bool FinishedLaunching(
        UIApplication app,
        NSDictionary options)
    {
        global::Xamarin.Forms.Forms.SetFlags("CollectionView_Experimental");
        global::Xamarin.Forms.Forms.Init();

        LoadApplication(new App());

        return base.FinishedLaunching(app, options);
    }
}

不要再执行以下代码:

window = new UIWindow(UIScreen.MainScreen.Bounds);
window.RootViewController = new UIViewController();
window.MakeKeyAndVisible();

这段代码会创建一个与 Xamarin.Forms Shell 无关的新窗口。它的空控制器不包含 Shell,也没有 Shell 当前的导航状态,还可能取代原窗口成为 key window。

获取 Xamarin.Forms 的原生根控制器

在 iOS 项目中,可以优先通过 AppDelegate.Window 获取 Xamarin.Forms 使用的窗口,同时为其他生命周期状态提供后备查找:

using System.Linq;
using UIKit;

public static class ViewControllerLocator
{
    public static UIViewController GetRootViewController()
    {
        var appDelegate =
            UIApplication.SharedApplication.Delegate as AppDelegate;

        var root = appDelegate?.Window?.RootViewController;
        if (root != null)
            return root;

        var window = UIApplication.SharedApplication.Windows
            .FirstOrDefault(x => x.IsKeyWindow)
            ?? UIApplication.SharedApplication.Windows
                .FirstOrDefault(x => !x.Hidden);

        return window?.RootViewController;
    }
}

如果支付请求可能在应用刚启动、恢复前台或 Shell 页面尚未显示时触发,root view controller 仍可能暂时为空。遇到这种情况,应延后支付操作,不要创建新窗口来顶替它。

找到当前可用于展示页面的控制器

Shell 的 root view controller 通常是容器控制器。直接对根控制器调用 PresentViewController,可能出现“控制器不在 window hierarchy 中”的错误,或者新页面被其他 modal 覆盖。

可以递归查找当前最上层的控制器:

using UIKit;

public static class ViewControllerLocator
{
    public static UIViewController GetRootViewController()
    {
        var appDelegate =
            UIApplication.SharedApplication.Delegate as AppDelegate;

        var root = appDelegate?.Window?.RootViewController;
        if (root != null)
            return root;

        foreach (var window in UIApplication.SharedApplication.Windows)
        {
            if (window.IsKeyWindow && window.RootViewController != null)
                return window.RootViewController;
        }

        return null;
    }

    public static UIViewController GetVisibleViewController(
        UIViewController controller = null)
    {
        controller ??= GetRootViewController();

        if (controller == null)
            return null;

        if (controller.PresentedViewController != null)
        {
            return GetVisibleViewController(
                controller.PresentedViewController);
        }

        if (controller is UINavigationController navigationController)
        {
            return GetVisibleViewController(
                navigationController.VisibleViewController
                ?? navigationController.TopViewController);
        }

        if (controller is UITabBarController tabBarController)
        {
            return GetVisibleViewController(
                tabBarController.SelectedViewController);
        }

        foreach (var child in controller.ChildViewControllers)
        {
            if (child.ViewIfLoaded?.Window != null)
                return GetVisibleViewController(child);
        }

        return controller;
    }

    public static UINavigationController FindNavigationController(
        UIViewController controller)
    {
        if (controller == null)
            return null;

        if (controller is UINavigationController navigationController)
            return navigationController;

        if (controller.NavigationController != null)
            return controller.NavigationController;

        if (controller.PresentedViewController != null)
        {
            var presentedResult = FindNavigationController(
                controller.PresentedViewController);

            if (presentedResult != null)
                return presentedResult;
        }

        foreach (var child in controller.ChildViewControllers)
        {
            var childResult = FindNavigationController(child);
            if (childResult != null)
                return childResult;
        }

        return null;
    }
}

ChildViewControllers 的具体结构属于 Xamarin.Forms Shell 的内部实现,不要依赖固定下标或某个私有类型。上面的代码只按照 UIKit 的标准容器关系查找控制器。

在 App 内展示支付页面

如果 payObj.Checkout(payment) 返回可嵌入的 UIView,可以把它加入 UIViewController,再通过当前可见的控制器展示:

using System;
using CoreGraphics;
using Foundation;
using UIKit;

public void PaymentRequest(PaymentData paymentData)
{
    UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
    {
        var payment = new Payment
        {
            RefNo = paymentData.ReferenceNumber
            // 设置其他支付参数
        };

        var payObject = new PayObject();
        // 设置 payObject 的其他参数

        UIView paymentView;

        try
        {
            paymentView = payObject.Checkout(payment);
        }
        catch (Exception ex)
        {
            // 按项目的日志和错误处理规范处理
            System.Diagnostics.Debug.WriteLine(ex);
            return;
        }

        if (paymentView == null)
            return;

        var presenter =
            ViewControllerLocator.GetVisibleViewController();

        if (presenter == null || presenter.View?.Window == null)
        {
            // 当前原生页面尚未进入 window hierarchy,应稍后重试或提示用户。
            return;
        }

        var paymentController = new UIViewController();

        paymentView.Frame = paymentController.View.Bounds;
        paymentView.AutoresizingMask =
            UIViewAutoresizing.FlexibleWidth |
            UIViewAutoresizing.FlexibleHeight;

        paymentController.View.AddSubview(paymentView);
        paymentController.ModalPresentationStyle =
            UIModalPresentationStyle.FullScreen;

        presenter.PresentViewController(
            paymentController,
            true,
            null);
    });
}

不要先给一个 UIView 设置 Frame,再用 SDK 的返回值覆盖该变量:

UIView responseView = new UIView();
responseView.Frame = UIScreen.MainScreen.Bounds;
responseView = payObj.Checkout(payment);

这里的 Frame 设置在第一个临时对象上,对 SDK 返回的 responseView 没有作用。应该先取得返回值,再设置布局。

SDK 要求 UINavigationController 时

不能通过下面的 Xamarin.Forms API,把原生控制器直接推入 Shell:

Shell.Current.Navigation.PushAsync(...)

PushAsync 接收 Xamarin.Forms Page,不接受 UIKit 的 UIViewController。

如果当前控制器位于原生 UINavigationController 中,可以这样处理:

UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
{
    var visibleController =
        ViewControllerLocator.GetVisibleViewController();

    var navigationController =
        ViewControllerLocator.FindNavigationController(
            visibleController);

    if (navigationController != null)
    {
        navigationController.PushViewController(
            paymentController,
            true);
        return;
    }

    var paymentNavigationController =
        new UINavigationController(paymentController);

    paymentNavigationController.ModalPresentationStyle =
        UIModalPresentationStyle.FullScreen;

    visibleController?.PresentViewController(
        paymentNavigationController,
        true,
        null);
});

在 Shell 项目中,不能假定顶层一定有可直接操作的 UINavigationController。对于一次性支付流程,用 full-screen modal 展示包装后的 UINavigationController,通常比直接修改 Shell 内部导航栈更稳妥。

还需要确认支付 SDK 的展示模式

如果按上述方式处理后,支付流程仍然会“跳出 App”,需要检查支付 SDK 的接口约定。下面这些情况与 root view controller 无关:

  • SDK 内部调用了 UIApplication.OpenUrl;
  • SDK 使用外部浏览器完成支付;
  • SDK 跳转到银行、钱包或支付平台 App;
  • SDK 使用 ASWebAuthenticationSession,由系统管理认证窗口;
  • SDK 要求传入 presenting view controller,但当前调用的重载没有传入;
  • SDK 返回的 UIView 只是启动入口,后续支付步骤仍通过外部 URL 完成。

如果 SDK 提供类似下面的接口,应优先传入当前可见的控制器:

var presenter =
    ViewControllerLocator.GetVisibleViewController();

payObject.Checkout(payment, presenter);

这只是接口形式示意。实际方法名和参数必须以该支付 SDK 的 iOS 文档为准,不能自行假定这个重载存在。

如果支付平台出于安全、认证或 App Store 合规要求,规定支付流程必须跳转外部页面,就无法通过修改 Xamarin.Forms 或 UIKit 导航代码强制内嵌。此时应配置 URL Scheme、Universal Link 或 SDK 回调,让用户在支付完成后返回应用。

注意事项

所有 UIKit 操作都必须在 iOS 主线程执行。支付按钮也应等到 Shell 页面完全显示后再允许触发,以免控制器尚未进入 window hierarchy。

关闭 modal 支付页面时,应调用:

paymentController.DismissViewController(true, null);

如果使用原生 push,则调用:

navigationController.PopViewController(true);

展示方式和关闭方式需要配对。不要用 PresentViewController 打开页面,再用 Shell 的 PopAsync 关闭。Xamarin.Forms Shell 负责 Forms 页面导航,UIKit 负责第三方原生控制器。两套导航可以同时存在,但同一个页面的打开和关闭流程不能混用。

备注:内容仅供参考。