1
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
|
||||
#import "KBFullAccessGuideView.h"
|
||||
#import "Masonry.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
|
||||
@interface KBFullAccessGuideView ()
|
||||
@property (nonatomic, strong) UIControl *backdrop;
|
||||
@@ -146,21 +147,12 @@
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (UIInputViewController *)kb_findInputController {
|
||||
UIResponder *res = self;
|
||||
while (res) {
|
||||
if ([res isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)res;
|
||||
}
|
||||
res = res.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
|
||||
- (void)onTapGoEnable {
|
||||
// 在扩展中无法使用 UIApplication。改为委托宿主打开链接:
|
||||
// 方案:优先拉起主 App 并由主 App 打开设置页,避免宿主拦截。
|
||||
UIInputViewController *ivc = [self kb_findInputController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { [self dismiss]; return; }
|
||||
|
||||
// 先尝试 Universal Link(如未配置可改为你的域名),失败再用自定义 scheme。
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
#import "KBFunctionView.h"
|
||||
#import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具
|
||||
#import "KBFunctionBarView.h"
|
||||
#import "KBFunctionPasteView.h"
|
||||
#import "KBFunctionTagCell.h"
|
||||
@@ -169,7 +170,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
[KBHUD showInfo:@"处理中…"];
|
||||
// return;
|
||||
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) return;
|
||||
|
||||
NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @"";
|
||||
@@ -268,14 +269,14 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
}
|
||||
- (void)onTapDelete {
|
||||
NSLog(@"点击:删除");
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy deleteBackward];
|
||||
}
|
||||
- (void)onTapClear {
|
||||
NSLog(@"点击:清空");
|
||||
// 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容)
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
// 逐批读取 documentContextBeforeInput 并删除,避免 50 字符窗口限制带来的残留
|
||||
NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞
|
||||
@@ -292,7 +293,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
- (void)onTapSend {
|
||||
NSLog(@"点击:发送");
|
||||
// 发送:插入换行。大多数聊天类 App 会把回车视为“发送”
|
||||
UIInputViewController *ivc = [self findInputViewController];
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
[proxy insertText:@"\n"];
|
||||
}
|
||||
@@ -417,16 +418,6 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
|
||||
|
||||
#pragma mark - Find Owner Controller
|
||||
|
||||
// 在视图的响应链中查找宿主 UIInputViewController(KeyboardViewController)
|
||||
- (UIInputViewController *)findInputViewController {
|
||||
UIResponder *responder = self;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
// 工具方法已提取到 KBResponderUtils.h
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#import "KBKeyboardView.h"
|
||||
#import "KBKeyButton.h"
|
||||
#import "KBKey.h"
|
||||
#import "KBResponderUtils.h" // 封装的响应链工具
|
||||
|
||||
@interface KBKeyboardView ()
|
||||
@property (nonatomic, strong) UIView *row1;
|
||||
@@ -13,6 +14,8 @@
|
||||
@property (nonatomic, strong) UIView *row3;
|
||||
@property (nonatomic, strong) UIView *row4;
|
||||
@property (nonatomic, strong) NSArray<NSArray<KBKey *> *> *keysForRows;
|
||||
// 长按退格的一次次删除控制标记(不使用 NSTimer,仅用 GCD 递归调度)
|
||||
@property (nonatomic, assign) BOOL backspaceHoldActive;
|
||||
@end
|
||||
|
||||
@implementation KBKeyboardView
|
||||
@@ -196,6 +199,16 @@
|
||||
[btn addTarget:self action:@selector(onKeyTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[row addSubview:btn];
|
||||
|
||||
// ⌫ 长按:开始连续逐个删除(无需 NSTimer)。使用 UILongPressGestureRecognizer 识别长按,
|
||||
// 在开始态触发递归的轻量调度,每次删除 1 个字符,直到松手或无内容。
|
||||
if (key.type == KBKeyTypeBackspace) {
|
||||
UILongPressGestureRecognizer *lp = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(onBackspaceLongPress:)];
|
||||
// 稍短的判定时间,提升响应(默认约 0.5s)。
|
||||
lp.minimumPressDuration = 0.35;
|
||||
lp.cancelsTouchesInView = YES; // 被识别为长按时,取消普通点击
|
||||
[btn addGestureRecognizer:lp];
|
||||
}
|
||||
|
||||
// Shift 按钮选中态随大小写状态变化
|
||||
if (key.type == KBKeyTypeShift) {
|
||||
btn.selected = self.shiftOn;
|
||||
@@ -293,6 +306,42 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 长按退格:按住时以小间隔逐个删除;松手停止。(不使用 NSTimer/DisplayLink)
|
||||
- (void)onBackspaceLongPress:(UILongPressGestureRecognizer *)gr {
|
||||
switch (gr.state) {
|
||||
case UIGestureRecognizerStateBegan: {
|
||||
self.backspaceHoldActive = YES;
|
||||
[self kb_backspaceStep];
|
||||
} break;
|
||||
case UIGestureRecognizerStateEnded:
|
||||
case UIGestureRecognizerStateCancelled:
|
||||
case UIGestureRecognizerStateFailed: {
|
||||
self.backspaceHoldActive = NO;
|
||||
} break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
// 单步删除并在需要时安排下一次,直到松手或无内容
|
||||
- (void)kb_backspaceStep {
|
||||
if (!self.backspaceHoldActive) { return; }
|
||||
UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
if (!ivc) { self.backspaceHoldActive = NO; return; }
|
||||
id<UITextDocumentProxy> proxy = ivc.textDocumentProxy;
|
||||
NSString *before = proxy.documentContextBeforeInput ?: @"";
|
||||
if (before.length <= 0) { self.backspaceHoldActive = NO; return; }
|
||||
[proxy deleteBackward]; // 每次仅删 1 个
|
||||
|
||||
// 轻量递归调度下一次;不使用 NSTimer,避免复杂状态管理
|
||||
__weak typeof(self) weakSelf = self;
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.06 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
__strong typeof(weakSelf) selfStrong = weakSelf;
|
||||
[selfStrong kb_backspaceStep];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Lazy
|
||||
|
||||
- (UIView *)row1 { if (!_row1) _row1 = [UIView new]; return _row1; }
|
||||
|
||||
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
28
CustomKeyboard/View/KBResponderUtils.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// KBResponderUtils.h
|
||||
// CustomKeyboard
|
||||
//
|
||||
// 统一封装:从任意 UIView/UIResponder 起,向响应链上查找 UIInputViewController。
|
||||
// 作为 header‑only 的工具,便于多处直接引入使用。
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#ifndef KBResponderUtils_h
|
||||
#define KBResponderUtils_h
|
||||
|
||||
/// 从给定 responder 开始,沿响应链查找宿主 UIInputViewController。
|
||||
/// 用法:UIInputViewController *ivc = KBFindInputViewController(self);
|
||||
static inline UIInputViewController *KBFindInputViewController(UIResponder *start) {
|
||||
UIResponder *responder = start;
|
||||
while (responder) {
|
||||
if ([responder isKindOfClass:[UIInputViewController class]]) {
|
||||
return (UIInputViewController *)responder;
|
||||
}
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
#endif /* KBResponderUtils_h */
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = "<group>"; };
|
||||
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = "<group>"; };
|
||||
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = "<group>"; };
|
||||
04A9FE142EB873C80020DB6D /* UIViewController+Extension.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIViewController+Extension.h"; sourceTree = "<group>"; };
|
||||
@@ -266,6 +267,7 @@
|
||||
children = (
|
||||
04C6EADB2EAF8CEB0089C901 /* KBToolBar.h */,
|
||||
04C6EADC2EAF8CEB0089C901 /* KBToolBar.m */,
|
||||
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */,
|
||||
04FC95682EB05497007BD342 /* KBKeyButton.h */,
|
||||
04FC95692EB05497007BD342 /* KBKeyButton.m */,
|
||||
04FC956B2EB054B7007BD342 /* KBKeyboardView.h */,
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:app.tknb.net</string>
|
||||
</array>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)com.loveKey.nyx.shared</string>
|
||||
|
||||
Reference in New Issue
Block a user