// // KBExtensionAppLauncher.m // CustomKeyboard // #import "KBExtensionAppLauncher.h" #if KB_URL_BRIDGE_ENABLE #import #endif @implementation KBExtensionAppLauncher #if KB_URL_BRIDGE_ENABLE + (BOOL)kb_openURLViaResponderChain:(NSURL *)url source:(nullable UIResponder *)source completion:(void (^ _Nullable)(BOOL success))completion { if (!url) { if (completion) { completion(NO); } return NO; } UIResponder *responder = source; // 优先尝试 openURL:options:completionHandler: // 注意:在键盘扩展里走“响应链兜底”本身就存在不确定性;不同系统/宿主 App 的实现 // 可能对 options 参数的类型有不同假设。为避免类型不匹配导致崩溃,options 统一传 nil。 SEL openURLOptionsSel = NSSelectorFromString(@"openURL:options:completionHandler:"); while (responder) { if ([responder respondsToSelector:openURLOptionsSel]) { void (*msgSend)(id, SEL, NSURL *, id, void (^)(BOOL)) = (void *)objc_msgSend; msgSend(responder, openURLOptionsSel, url, nil, ^(BOOL ok) { if (completion) { completion(ok); } }); return YES; } responder = responder.nextResponder; } // 尝试 openURL:completionHandler: responder = source; SEL openURLCompletionSel = NSSelectorFromString(@"openURL:completionHandler:"); while (responder) { if ([responder respondsToSelector:openURLCompletionSel]) { void (*msgSend)(id, SEL, NSURL *, void (^)(BOOL)) = (void *)objc_msgSend; msgSend(responder, openURLCompletionSel, url, ^(BOOL ok) { if (completion) { completion(ok); } }); return YES; } responder = responder.nextResponder; } // 兜底:openURL: responder = source; SEL openURLSel = NSSelectorFromString(@"openURL:"); while (responder) { if ([responder respondsToSelector:openURLSel]) { BOOL (*msgSend)(id, SEL, NSURL *) = (void *)objc_msgSend; BOOL ok = msgSend(responder, openURLSel, url); if (completion) { completion(ok); } return YES; } responder = responder.nextResponder; } if (completion) { completion(NO); } return NO; } #endif + (void)openPrimaryURL:(NSURL * _Nullable)primaryURL fallbackURL:(NSURL * _Nullable)fallbackURL usingInputController:(UIInputViewController *)ivc source:(UIResponder * _Nullable)source completion:(void (^ _Nullable)(BOOL success))completion { if (![NSThread isMainThread]) { dispatch_async(dispatch_get_main_queue(), ^{ [self openPrimaryURL:primaryURL fallbackURL:fallbackURL usingInputController:ivc source:source completion:completion]; }); return; } 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; } #if KB_URL_BRIDGE_ENABLE // 的场景且业务强依赖时才开启此兜底。 UIResponder *start = (source ?: (UIResponder *)ivc.view ?: (UIResponder *)ivc); [self kb_openURLViaResponderChain:second source:start completion:^(BOOL ok3) { finish(ok3); }]; #else finish(NO); #endif }]; } else { #if KB_URL_BRIDGE_ENABLE UIResponder *start = (source ?: (UIResponder *)ivc.view ?: (UIResponder *)ivc); [self kb_openURLViaResponderChain:first source:start completion:^(BOOL ok3) { finish(ok3); }]; #else finish(NO); #endif } }]; } + (void)openScheme:(NSURL *)scheme usingInputController:(UIInputViewController *)ivc source:(UIResponder * _Nullable)source completion:(void (^ _Nullable)(BOOL success))completion { [self openPrimaryURL:scheme fallbackURL:nil usingInputController:ivc source:source completion:completion]; } @end