处理键盘不能拉起主app的问题

This commit is contained in:
2025-11-05 18:10:56 +08:00
parent f43f94b94d
commit 7a1b17d060
14 changed files with 270 additions and 52 deletions

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>kbkeyboardAppExtension</string>
</array>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>

View File

@@ -52,8 +52,17 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
- (void)setupUI {
//
[self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT].active = YES;
// High
NSLayoutConstraint *h = [self.view.heightAnchor constraintEqualToConstant:KEYBOARDHEIGHT];
h.priority = UILayoutPriorityDefaultHigh; // 750
h.active = YES;
// UIInputView
if ([self.view isKindOfClass:[UIInputView class]]) {
UIInputView *iv = (UIInputView *)self.view;
if ([iv respondsToSelector:@selector(setAllowsSelfSizing:)]) {
iv.allowsSelfSizing = NO;
}
}
//
[self.view addSubview:self.bgImageView];
[self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) {
@@ -222,7 +231,8 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
}
- (void)kb_tryOpenContainerForLoginIfNeeded {
NSURL *url = [NSURL URLWithString:@"kbkeyboard://login?src=keyboard"];
// 使 App Scheme
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
if (!url) return;
__weak typeof(self) weakSelf = self;
[self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {

View File

@@ -23,9 +23,15 @@
// 通用链接Universal Links统一配置
// 配置好 AASA 与 Associated Domains 后,只需修改这里即可切换域名/path。
#define KB_UL_BASE @"https://your.domain/ul" // 替换为你的真实域名与前缀路径
#define KB_UL_BASE @"https://app.tknb.net/ul" // 与 Associated Domains 一致
#define KB_UL_LOGIN KB_UL_BASE @"/login"
#define KB_UL_SETTINGS KB_UL_BASE @"/settings"
// 在扩展内,启用 URL Bridge仅在显式的用户点击动作中使用
// 这样即便宿主 App如备忘录拒绝 extensionContext 的 openURL仍可通过响应链兜底拉起容器 App。
#ifndef KB_URL_BRIDGE_ENABLE
#define KB_URL_BRIDGE_ENABLE 1
#endif
#endif /* PrefixHeader_pch */

View File

@@ -0,0 +1,30 @@
//
// 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

@@ -0,0 +1,46 @@
//
// 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

View File

@@ -6,10 +6,14 @@
#import "KBFullAccessGuideView.h"
#import "Masonry.h"
#import "KBResponderUtils.h" // UIInputViewController
#import "KBHUD.h"
#import "KBURLOpenBridge.h"
@interface KBFullAccessGuideView ()
@property (nonatomic, strong) UIControl *backdrop;
@property (nonatomic, strong) UIView *card;
//
@property (nonatomic, weak) UIInputViewController *ivc;
@end
@implementation KBFullAccessGuideView
@@ -110,7 +114,8 @@
}
- (void)presentIn:(UIView *)parent {
UIView *container = parent.window ?: parent;
if (!parent) return;
UIView *container = parent; // window
self.frame = container.bounds;
self.alpha = 0;
[container addSubview:self];
@@ -126,15 +131,19 @@
+ (void)showInView:(UIView *)parent {
if (!parent) return;
//
for (UIView *v in (parent.window ?: parent).subviews) {
// parent
for (UIView *v in parent.subviews) {
if ([v isKindOfClass:[KBFullAccessGuideView class]]) return;
}
[[KBFullAccessGuideView build] presentIn:parent];
KBFullAccessGuideView *view = [KBFullAccessGuideView build];
// ivc
view.ivc = KBFindInputViewController(parent);
[view presentIn:parent];
}
+ (void)dismissFromView:(UIView *)parent {
UIView *container = parent.window ?: parent;
UIView *container = parent;
if (!container) return;
for (UIView *v in container.subviews) {
if ([v isKindOfClass:[KBFullAccessGuideView class]]) {
[(KBFullAccessGuideView *)v dismiss];
@@ -148,30 +157,73 @@
#pragma mark - Actions
// KBResponderUtils.h
// App访 Scheme UL
- (void)onTapGoEnable {
// 使 UIApplication宿
// App App 宿
UIInputViewController *ivc = KBFindInputViewController(self);
if (!ivc) { [self dismiss]; return; }
// Universal Link scheme
// SchemeApp openURL
// 使 App Scheme
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//settings?src=kb_extension", KB_APP_SCHEME]];
// Universal Link AASA/Associated Domains KB_UL_BASE
NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=kb_extension", KB_UL_SETTINGS]];
void (^fallback)(void) = ^{
NSURL *scheme = [NSURL URLWithString:@"kbkeyboard://settings?src=kb_extension"]; // App openURL
[ivc.extensionContext openURL:scheme completionHandler:^(__unused BOOL ok2) {
//
[self dismiss];
}];
void (^finish)(BOOL) = ^(BOOL ok){
if (ok) { [self dismiss]; }
else {
[KBHUD showInfo:@"无法自动打开,请按路径:设置→通用→键盘→键盘→恋爱键盘→允许完全访问"]; //
}
};
// Scheme宿 App
if (scheme) {
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok) {
if (ok) { finish(YES); return; }
if (ul) {
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok2) {
if (ok2) { finish(YES); return; }
// openURL:
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
if (!bridged && ul) {
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
}
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}];
} else {
// UL Scheme
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}
}];
return;
}
// scheme UL
if (ul) {
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) { [self dismiss]; }
else { fallback(); }
if (ok) { finish(YES); return; }
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:ul from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
finish(bridged);
}];
} else {
fallback();
finish(NO);
}
}
@end

View File

@@ -15,6 +15,7 @@
#import "KBFullAccessGuideView.h"
#import "KBFullAccessManager.h"
#import "KBSkinManager.h"
#import "KBURLOpenBridge.h" // openURL:
static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
@@ -49,6 +50,9 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
//
_lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount;
// 访访 TCC/XPC
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil];
}
return self;
}
@@ -64,6 +68,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
- (void)dealloc {
[self stopPasteboardMonitor];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - UI
@@ -92,10 +97,11 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
[self.rightButtonContainer addSubview:self.clearButtonInternal];
[self.rightButtonContainer addSubview:self.sendButtonInternal];
//
//
CGFloat smallH = 44;
CGFloat bigH = 56;
CGFloat vSpace = 10;
// 10 276 8 AutoLayout
CGFloat vSpace = 8;
[self.pasteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.rightButtonContainer.mas_top);
make.left.right.equalTo(self.rightButtonContainer);
@@ -114,8 +120,10 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
[self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace);
make.left.right.equalTo(self.rightButtonContainer);
make.height.mas_equalTo(bigH);
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom); //
// smallH
make.height.greaterThanOrEqualTo(@(smallH));
make.height.lessThanOrEqualTo(@(bigH));
make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom);
}];
// 2.
@@ -193,9 +201,22 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
[ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) {
if (ok) return; // Universal Link
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"kbkeyboard://login?src=functionView&index=%ld&title=%@", (long)indexPath.item, encodedTitle]];
// 使 App Scheme
NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)indexPath.item, encodedTitle]];
[ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) {
if (!ok2) {
if (ok2) return;
// openURL:
// 宿
BOOL bridged = NO;
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
bridged = [KBURLOpenBridge openURLViaResponder:scheme from:self];
#pragma clang diagnostic pop
} @catch (__unused NSException *e) { bridged = NO; }
if (!bridged) {
// 访宿 Manager
dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; });
}
@@ -235,6 +256,8 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
// - / changeCount
- (void)startPasteboardMonitor {
// 访宿/
if (![[KBFullAccessManager shared] hasFullAccess]) return;
if (self.pasteboardTimer) return;
__weak typeof(self) weakSelf = self;
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
@@ -259,24 +282,30 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
- (void)didMoveToWindow {
[super didMoveToWindow];
if (self.window && !self.isHidden) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
}
[self kb_refreshPasteboardMonitor];
}
- (void)setHidden:(BOOL)hidden {
BOOL wasHidden = self.isHidden;
[super setHidden:hidden];
if (wasHidden != hidden) {
if (!hidden && self.window) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
}
[self kb_refreshPasteboardMonitor];
}
}
// 访
- (void)kb_refreshPasteboardMonitor {
BOOL visible = (self.window && !self.isHidden);
if (visible && [[KBFullAccessManager shared] hasFullAccess]) {
[self startPasteboardMonitor];
} else {
[self stopPasteboardMonitor];
}
}
- (void)kb_fullAccessChanged {
dispatch_async(dispatch_get_main_queue(), ^{ [self kb_refreshPasteboardMonitor]; });
}
- (void)onTapDelete {
NSLog(@"点击:删除");
UIInputViewController *ivc = KBFindInputViewController(self);

View File

@@ -264,6 +264,9 @@
if (firstChar) {
for (KBKeyButton *b in row.subviews) {
if (![b isKindOfClass:[KBKeyButton class]]) continue;
// firstChar
// self == self * k
if (b == firstChar) continue;
if (b.key.type == KBKeyTypeCharacter) continue;
CGFloat multiplier = 1.5;
if (b.key.type == KBKeyTypeSpace) multiplier = 4.0;