PAYATHON 2026

如何避免 Xamarin.Forms iOS 支付页面在新窗口打开?

支付老李

结论

不要在 AppDelegate 中另外创建 UIWindow,也不要把支付视图挂到空的 UIViewController 上。Xamarin.Forms 启动完成后已经创建了应用窗口和控制器层级。支付页面应从这个窗口找到当前真正可见的 UIViewController,再以模态方式展示,或者通过当前 UINavigationController 调用 PushViewController。

具体步骤如下:

  1. 等待 FinishedLaunching 完成。
  2. 使用 Xamarin.Forms 创建的 Window。
  3. 从 RootViewController 开始,递归查找当前可见的控制器。
  4. 在 iOS 主线程中展示支付页面。

为什么会出现新窗口或空的 RootViewController

FormsApplicationDelegate 仍然属于 iOS 应用委托体系。它没有直接继承 UIApplicationDelegate,并不代表它无法拥有窗口。调用以下代码后:

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

Xamarin.Forms 会创建自己的 UIWindow、根控制器以及 Shell 对应的导航层级。

问题通常出在获取窗口的时机或方式上:

  • 如果在 FinishedLaunching 结束前读取 KeyWindow,窗口可能还没有成为 key window。
  • 在较新的 iOS 窗口模型中,UIApplication.SharedApplication.KeyWindow 已经不可靠。
  • Shell 的根控制器不一定是当前可见的控制器。
  • 页面上可能已有模态控制器、UINavigationController 或 UITabBarController。
  • 手动创建第二个 UIWindow 会绕过 Xamarin.Forms 已有的页面层级,因此支付页面看起来像是出现在另一个窗口中。

所以应删除下面这段代码:

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

这段代码不会修复 Xamarin.Forms 的窗口,只会创建一个与 Shell 页面层级无关的新窗口。

保持 AppDelegate 使用 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);
    }
}

支付操作应当在应用启动完成、Shell 页面已经显示后触发。例如,可以通过页面按钮或 ViewModel 命令调用,不要在 FinishedLaunching 中立即展示。

获取 Xamarin.Forms 当前可见的 UIViewController

可以在 iOS 项目中添加一个辅助类。它先从 Xamarin.Forms 的 AppDelegate.Window 获取根控制器,再解析当前可见的控制器:

using System.Linq;
using UIKit;

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

        var window = appDelegate?.Window;

        if (window == null)
        {
            window = UIApplication.SharedApplication.Windows
                .FirstOrDefault(x => x.IsKeyWindow);
        }

        var rootViewController = window?.RootViewController;

        return GetVisibleViewController(rootViewController);
    }

    private static UIViewController GetVisibleViewController(
        UIViewController viewController)
    {
        if (viewController == null)
            return null;

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

        if (viewController is UINavigationController navigationController)
        {
            return GetVisibleViewController(
                navigationController.VisibleViewController);
        }

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

        foreach (var child in viewController.ChildViewControllers)
        {
            var visibleChild = GetVisibleViewController(child);

            if (visibleChild != null)
                return visibleChild;
        }

        return viewController;
    }

    public static UINavigationController GetNavigationController()
    {
        var visibleViewController = GetVisibleViewController();

        if (visibleViewController is UINavigationController navigationController)
            return navigationController;

        return visibleViewController?.NavigationController;
    }
}

这里优先使用 AppDelegate.Window,因为它指向 Xamarin.Forms 创建并管理的窗口。Windows.FirstOrDefault(x => x.IsKeyWindow) 只作为后备方案。

如果应用明确启用了 iOS Scene 生命周期,应从当前活动的 UIWindowScene 获取窗口。不过,传统的 Xamarin.Forms 5 项目通常仍由 AppDelegate.Window 管理窗口。

在应用内部模态显示支付页面

如果支付 SDK 的 Checkout 返回 UIView,可以把它放进一个控制器,再通过当前可见的控制器展示:

using CoreGraphics;
using Foundation;
using UIKit;
using Xamarin.Forms;

public void PaymentRequest(PaymentData paymentData)
{
    Device.BeginInvokeOnMainThread(() =>
    {
        var payment = new Payment
        {
            RefNo = paymentData.ReferenceNumber
        };

        var payObj = new PayObject();

        UIView paymentView = payObj.Checkout(payment);

        if (paymentView == null)
            return;

        var presenter = ViewControllerHelper.GetVisibleViewController();

        if (presenter == null)
            return;

        var paymentViewController = new UIViewController();

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

        paymentViewController.View.AddSubview(paymentView);

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

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

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

最后一行会丢掉此前对 responseView 的所有设置。应先取得 SDK 返回的视图,再设置它的尺寸和自动调整规则。

SDK 要求 Push 时的处理方式

如果支付 SDK 明确要求把页面压入导航栈,而不是以模态方式展示,就需要取得 Shell 当前页面对应的 UINavigationController:

Device.BeginInvokeOnMainThread(() =>
{
    UIView paymentView = payObj.Checkout(payment);

    if (paymentView == null)
        return;

    var paymentViewController = new UIViewController();

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

    paymentViewController.View.AddSubview(paymentView);

    var navigationController =
        ViewControllerHelper.GetNavigationController();

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

    var presenter =
        ViewControllerHelper.GetVisibleViewController();

    presenter?.PresentViewController(
        paymentViewController,
        true,
        null);
});

容器类型应以支付 SDK 的要求为准。有些 SDK 只返回 UIView,适合由应用自行包装;另一些 SDK 会返回完整的 UIViewController。如果 SDK 已经返回控制器,直接展示即可,不要再套一层空控制器。

仍然跳到应用外部时需要检查什么

如果已经通过正确的可见控制器展示支付页面,但支付流程仍然打开 Safari、SFSafariViewController 或其他应用,问题可能不在 Xamarin.Forms 的根控制器,而在支付 SDK 本身的设计。例如:

  • SDK 的 Checkout 内部调用了 OpenUrl。
  • 支付方式要求跳转到银行应用或系统浏览器。
  • SDK 需要传入宿主 UIViewController,但当前调用的是返回 UIView 的重载。
  • 回调 URL、URL Scheme 或 Universal Link 配置不正确。
  • SDK 同时提供“内嵌支付”和“外部浏览器支付”接口,而当前使用的是后者。

此时应检查 SDK 文档,确认是否提供接收 UIViewController、UINavigationController 或 presentation context 的初始化方法。宿主控制器应传入 ViewControllerHelper.GetVisibleViewController() 返回的对象。

不要强行替换 Xamarin.Forms 的根控制器。应使用 Xamarin.Forms 已经创建的窗口,并从现有控制器层级中找到当前可见且能够执行展示操作的控制器。

备注:内容仅供参考。