NativeScript iOS App Delegate Methods 未触发
结论
这些方法没有触发,通常不是 Mobile Pay SDK 本身的问题,而是 App Delegate 的注册方式或注册时机不对:
- 执行
@ObjCClass后,Objective-C 类已经完成注册。此时再向prototype动态添加方法,不一定能把对应的 Objective-C selector 暴露给 iOS。 - App Delegate 必须在 NativeScript 启动、Angular bootstrap 和原生 delegate 实例创建之前注册。
- 现代 iOS 通常会调用
application:openURL:options:,在 NativeScript 中对应applicationOpenURLOptions。applicationHandleOpenURL是较早的回调。 - 应用还需要正确配置 Mobile Pay 返回时使用的 URL Scheme,否则 iOS 不会把回调 URL 交给应用。
应创建一个明确实现 UIApplicationDelegate 的类,在类体中直接声明相关方法,并在入口文件启动 Angular 之前把该类赋给 application.ios.delegate。
为什么动态添加的方法收不到回调
下面的写法有一个问题:
@ObjCClass(UIApplicationDelegate)
class UIApplicationDelegateImpl extends UIResponder implements UIApplicationDelegate {
}
appDelegate.prototype.applicationHandleOpenURL = (...) => {
// ...
};
@ObjCClass 会把 JavaScript/TypeScript 类注册到 Objective-C runtime。注册发生时,类中还没有 applicationHandleOpenURL、applicationOpenURLOptions 等方法。
随后修改 JavaScript prototype,虽然能在 JavaScript 中看到这些函数,但 Objective-C runtime 未必会认为这个类实现了相应的 selector。因此,当 iOS 检查 delegate 是否响应 application:openURL:options: 时,可能会得到否定结果,也就不会调用这些方法。
如果这段代码在 Angular service、component 或插件初始化期间执行,App Delegate 的原生实例还可能已经创建。此时再替换或扩展 delegate,往往已经太晚。
推荐实现
把自定义 App Delegate 放在独立文件中,并将所有需要暴露给 iOS 的方法直接定义在类体内。
例如,创建 app-delegate.ts:
import { ios } from "tns-core-modules/application";
declare const MobilePayManager: any;
@ObjCClass(UIApplicationDelegate)
export class AppDelegate extends UIResponder implements UIApplicationDelegate {
public applicationDidFinishLaunchingWithOptions(
application: UIApplication,
launchOptions: NSDictionary<any, any>
): boolean {
return true;
}
// iOS 9 及更高版本通常调用此方法
public applicationOpenURLOptions(
application: UIApplication,
url: NSURL,
options: NSDictionary<string, any>
): boolean {
return this.handleMobilePayURL(url);
}
// 兼容旧版 iOS 回调
public applicationOpenURLSourceApplicationAnnotation(
application: UIApplication,
url: NSURL,
sourceApplication: string,
annotation: any
): boolean {
return this.handleMobilePayURL(url);
}
// 更早期的 iOS 回调
public applicationHandleOpenURL(
application: UIApplication,
url: NSURL
): boolean {
return this.handleMobilePayURL(url);
}
private handleMobilePayURL(url: NSURL): boolean {
if (!url) {
return false;
}
MobilePayManager
.sharedInstance()
.handleMobilePayCallbacksWithUrlSuccessErrorCancel(
url,
paymentSuccess,
paymentFailure,
paymentCancel
);
return true;
}
}
function paymentSuccess(result: any): void {
console.log("Mobile Pay payment succeeded", result);
}
function paymentFailure(error: any): void {
console.log("Mobile Pay payment failed", error);
}
function paymentCancel(result: any): void {
console.log("Mobile Pay payment cancelled", result);
}
然后在应用入口文件中完成注册。对于这个项目使用的旧版 NativeScript Angular,通常要在 main.ts 调用 bootstrapModule 之前注册:
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
import { ios } from "tns-core-modules/application";
import { AppModule } from "./app.module";
import { AppDelegate } from "./app-delegate";
if (ios) {
ios.delegate = AppDelegate;
}
platformNativeScriptDynamic().bootstrapModule(AppModule);
执行顺序不能颠倒。下面这行:
ios.delegate = AppDelegate;
必须放在这行之前:
platformNativeScriptDynamic().bootstrapModule(AppModule);
如果已有插件扩展了 App Delegate
直接替换 ios.delegate,可能会覆盖其他插件提供的实现。如果需要兼容现有 delegate,可以继承它,而不是创建一个与其无关的新类:
import { ios } from "tns-core-modules/application";
const ExistingAppDelegate =
ios.delegate || UIResponder;
@ObjCClass(UIApplicationDelegate)
class AppDelegate extends ExistingAppDelegate
implements UIApplicationDelegate {
public applicationOpenURLOptions(
application: UIApplication,
url: NSURL,
options: NSDictionary<string, any>
): boolean {
const handled = MobilePayManager
.sharedInstance()
.handleMobilePayCallbacksWithUrlSuccessErrorCancel(
url,
paymentSuccess,
paymentFailure,
paymentCancel
);
return handled === undefined ? true : !!handled;
}
public applicationHandleOpenURL(
application: UIApplication,
url: NSURL
): boolean {
MobilePayManager
.sharedInstance()
.handleMobilePayCallbacksWithUrlSuccessErrorCancel(
url,
paymentSuccess,
paymentFailure,
paymentCancel
);
return true;
}
}
ios.delegate = AppDelegate;
能否安全继承现有 delegate,取决于 NativeScript 版本和相关插件实现 App Delegate 的方式。如果现有 delegate 还负责处理通知、Firebase 或深链接,自定义实现就不能无条件接收所有 URL。更稳妥的处理方式是先判断 URL 是否属于 Mobile Pay。如果不属于,再交给原有实现。
检查 URL Scheme 配置
即使 App Delegate 配置正确,没有注册 URL Scheme 仍然收不到回调。需要在 App_Resources/iOS/Info.plist 中配置 Mobile Pay 返回应用时使用的 scheme,例如:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>mobilepay-callback</string>
<key>CFBundleURLSchemes</key>
<array>
<string>your-app-scheme</string>
</array>
</dict>
</array>
your-app-scheme 必须与传给 Mobile Pay SDK 的回调 scheme 完全一致。具体的 scheme 名称以及 Mobile Pay SDK 要求的其他 Info.plist 配置,应以当前使用的 SDK 版本接入说明为准。
可以先在处理函数中打印收到的 URL:
private handleMobilePayURL(url: NSURL): boolean {
console.log("Received callback URL:", url.absoluteString);
// 调用 Mobile Pay SDK
return true;
}
方法名称必须准确
NativeScript 会把 Objective-C selector 映射为连续的驼峰式方法名:
application:handleOpenURL:
→ applicationHandleOpenURL
application:openURL:options:
→ applicationOpenURLOptions
application:openURL:sourceApplication:annotation:
→ applicationOpenURLSourceApplicationAnnotation
下面的方法不是 UIApplicationDelegate 的 URL 回调:
openURL(url: NSURL): boolean
它不能代替前面的 delegate 方法,可以删除。
还需要注意的事项
- App Delegate 注册代码应放在入口模块中,不能等到页面、组件或 Angular service 创建后再执行。
- 不要先声明一个空的
@ObjCClass,再向其prototype添加需要暴露给 Objective-C 的方法。 - 优先实现
applicationOpenURLOptions,同时保留旧方法,以兼容旧系统或旧 SDK。 - 如果应用启用了基于 scene 的生命周期,回调也可能进入 Scene Delegate。不过这个项目使用 NativeScript 5.3,应先检查传统 App Delegate 和 URL Scheme 配置。
- 修改
Info.plist或原生 delegate 后,需要重新构建并安装应用,以免旧的原生构建产物影响验证。 - 如果回调函数依赖某个 service 实例,需要显式保存该实例或绑定上下文,不能假定 Objective-C 调用时的
this仍指向原来的 Angular service。
修正的重点是:在应用启动前注册 delegate,并把 URL 回调方法直接声明在 @ObjCClass 类中。Objective-C runtime 识别对应 selector 后,Mobile Pay 返回应用时,applicationOpenURLOptions 或兼容的 applicationHandleOpenURL 才能收到调用。
备注:内容仅供参考。
