如何避免 Xamarin.Forms iOS 支付页面在新窗口打开?
结论
不要在 AppDelegate 中另外创建 UIWindow,也不要把支付视图挂到空的 UIViewController 上。Xamarin.Forms 启动完成后已经创建了应用窗口和控制器层级。支付页面应从这个窗口找到当前真正可见的 UIViewController,再以模态方式展示,或者通过当前 UINavigationController 调用 PushViewController。
具体步骤如下:
- 等待
FinishedLaunching完成。 - 使用 Xamarin.Forms 创建的
Window。 - 从
RootViewController开始,递归查找当前可见的控制器。 - 在 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 已经创建的窗口,并从现有控制器层级中找到当前可见且能够执行展示操作的控制器。
备注:内容仅供参考。
