封装跨应用拉起,

This commit is contained in:
2025-11-21 18:26:02 +08:00
parent 0f4ca89060
commit fc87c545a0
11 changed files with 251 additions and 295 deletions

View File

@@ -0,0 +1,36 @@
//
// KBExtensionAppLauncher.h
// CustomKeyboard
//
// 封装:在键盘扩展中拉起主 AppScheme / Universal Link + 响应链兜底)。
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface KBExtensionAppLauncher : NSObject
/// 通用入口:优先尝试 primaryURL失败后尝试 fallbackURL
/// 两者都失败时再通过响应链openURL:)做兜底。
/// - Parameters:
/// - primaryURL: 第一优先尝试的 URL可为 Scheme 或 UL
/// - fallbackURL: 失败时的备用 URL可为 nil
/// - ivc: 当前的 UIInputViewController用于 extensionContext openURL
/// - source: 兜底时用作起点的 responder通常传 self 或 self.view
/// - completion: 最终是否“看起来已成功发起”打开动作(不保证一定跳转到 App
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
fallbackURL:(NSURL * _Nullable)fallbackURL
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion;
/// 简化版:只针对单一 Scheme 做尝试 + 响应链兜底。
+ (void)openScheme:(NSURL *)scheme
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,121 @@
//
// KBExtensionAppLauncher.m
// CustomKeyboard
//
#import "KBExtensionAppLauncher.h"
#import <objc/message.h>
@implementation KBExtensionAppLauncher
+ (void)openPrimaryURL:(NSURL * _Nullable)primaryURL
fallbackURL:(NSURL * _Nullable)fallbackURL
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion {
if (!ivc || (!primaryURL && !fallbackURL)) {
if (completion) { completion(NO); }
return;
}
// 线 dispatch
void (^finish)(BOOL) = ^(BOOL ok){
if (!completion) return;
if ([NSThread isMainThread]) {
completion(ok);
} else {
dispatch_async(dispatch_get_main_queue(), ^{ completion(ok); });
}
};
NSURL *first = primaryURL ?: fallbackURL;
NSURL *second = (first == primaryURL) ? fallbackURL : nil;
if (!first) {
finish(NO);
return;
}
[ivc.extensionContext openURL:first completionHandler:^(BOOL ok) {
if (ok) {
finish(YES);
return;
}
if (second) {
[ivc.extensionContext openURL:second completionHandler:^(BOOL ok2) {
if (ok2) {
finish(YES);
return;
}
BOOL bridged = [self p_bridgeFirst:first second:second from:source];
finish(bridged);
}];
} else {
BOOL bridged = [self p_bridgeFirst:first second:nil from:source];
finish(bridged);
}
}];
}
+ (void)openScheme:(NSURL *)scheme
usingInputController:(UIInputViewController *)ivc
source:(UIResponder *)source
completion:(void (^ _Nullable)(BOOL success))completion {
[self openPrimaryURL:scheme
fallbackURL:nil
usingInputController:ivc
source:source
completion:completion];
}
#pragma mark - Private
// openURL: KBURLOpenBridge
+ (BOOL)p_openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
#if KB_URL_BRIDGE_ENABLE
if (!url || !start) return NO;
SEL sel = NSSelectorFromString(@"openURL:");
UIResponder *responder = start;
while (responder) {
@try {
if ([responder respondsToSelector:sel]) {
BOOL handled = NO;
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
if (funcBool) {
handled = funcBool(responder, sel, url);
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[responder performSelector:sel withObject:url];
handled = YES;
#pragma clang diagnostic pop
}
return handled;
}
} @catch (__unused NSException *e) {
// ignore and continue
}
responder = responder.nextResponder;
}
return NO;
#else
(void)url; (void)start;
return NO;
#endif
}
+ (BOOL)p_bridgeFirst:(NSURL * _Nullable)first
second:(NSURL * _Nullable)second
from:(UIResponder *)source {
BOOL bridged = NO;
if (first) {
bridged = [self p_openURLViaResponder:first from:source];
}
if (!bridged && second) {
bridged = [self p_openURLViaResponder:second from:source];
}
return bridged;
}
@end

View File

@@ -1,30 +0,0 @@
//
// KBURLOpenBridge.h
// 非公开:通过响应链查找 `openURL:` 选择器,尝试在扩展环境中打开自定义 scheme。
// 警告:存在审核风险。默认仅 Debug 启用(见 KB_URL_BRIDGE_ENABLE
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
#ifndef KB_URL_BRIDGE_ENABLE
#if DEBUG
#define KB_URL_BRIDGE_ENABLE 1
#else
#define KB_URL_BRIDGE_ENABLE 0
#endif
#endif
@interface KBURLOpenBridge : NSObject
/// 尝试通过响应链调用 openURL:(仅在 KB_URL_BRIDGE_ENABLE 为 1 时执行)。
/// @param url 自定义 scheme如 kbkeyboard://settings
/// @param start 起始 responder传 self 或任意视图)
/// @return 是否看起来已发起打开动作(不保证一定成功)
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start;
@end
NS_ASSUME_NONNULL_END

View File

@@ -1,46 +0,0 @@
//
// KBURLOpenBridge.m
//
#import "KBURLOpenBridge.h"
#import <objc/message.h>
@implementation KBURLOpenBridge
+ (BOOL)openURLViaResponder:(NSURL *)url from:(UIResponder *)start {
#if KB_URL_BRIDGE_ENABLE
if (!url || !start) return NO;
SEL sel = NSSelectorFromString(@"openURL:");
UIResponder *responder = start;
while (responder) {
@try {
if ([responder respondsToSelector:sel]) {
// 退 performSelector
BOOL handled = NO;
// (BOOL)openURL:(NSURL *)
BOOL (*funcBool)(id, SEL, NSURL *) = (BOOL (*)(id, SEL, NSURL *))objc_msgSend;
if (funcBool) {
handled = funcBool(responder, sel, url);
} else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[responder performSelector:sel withObject:url];
handled = YES;
#pragma clang diagnostic pop
}
return handled;
}
} @catch (__unused NSException *e) {
// ignore and continue
}
responder = responder.nextResponder;
}
return NO;
#else
(void)url; (void)start;
return NO;
#endif
}
@end