diff --git a/CustomKeyboard/View/KBFunctionView.m b/CustomKeyboard/View/KBFunctionView.m index 605636d..1996698 100644 --- a/CustomKeyboard/View/KBFunctionView.m +++ b/CustomKeyboard/View/KBFunctionView.m @@ -34,6 +34,12 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; // 临时:点击标签后展示的“流式文本视图”与其删除按钮 @property (nonatomic, strong, nullable) KBStreamTextView *streamTextView; @property (nonatomic, strong, nullable) UIButton *streamDeleteButton; +// 流式测试数据 & 定时器 +@property (nonatomic, strong, nullable) NSTimer *streamMockTimer; +@property (nonatomic, strong, nullable) NSArray *streamMockChunks; +@property (nonatomic, assign) NSInteger streamMockIndex; +@property (nonatomic, copy, nullable) NSString *streamMockSource; // 连续文本源(包含 \t 作为分段) +@property (nonatomic, assign) NSInteger streamMockCursor; // 当前光标位置 // Data @property (nonatomic, strong) NSArray *itemsInternal; @@ -73,6 +79,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; - (void)dealloc { [self stopPasteboardMonitor]; + [self kb_stopMockStreaming]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } @@ -204,7 +211,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; [self addSubview:sv]; [sv mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.mas_left).offset(12); - make.right.equalTo(self).offset(-12); + make.right.equalTo(self.rightButtonContainer.mas_left).offset(-12); make.top.equalTo(self.pasteViewInternal.mas_bottom).offset(10); make.bottom.equalTo(self.mas_bottom).offset(-10); }]; @@ -226,10 +233,14 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; make.width.mas_greaterThanOrEqualTo(56); }]; self.streamDeleteButton = del; + + // 启动模拟“后端流”数据,用于查看 KBStreamTextView 展示效果 + [self kb_startMockStreamingWithSeed:title]; } - (void)kb_onTapStreamDelete { // 关闭并销毁流式视图,恢复标签列表 + [self kb_stopMockStreaming]; [self.streamDeleteButton removeFromSuperview]; self.streamDeleteButton = nil; [self.streamTextView removeFromSuperview]; @@ -237,6 +248,56 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; self.collectionViewInternal.hidden = NO; } +#pragma mark - Mock Streaming + +// 生成并按时间片推送模拟流数据到 KBStreamTextView +- (void)kb_startMockStreamingWithSeed:(NSString *)seedTitle { + [self kb_stopMockStreaming]; + + // 连续文本源(包含 \t 作为“分段”分隔符;\n 为换行),按 1~3 字随机切片推送 + NSString *t = seedTitle ?: @"高情商"; + self.streamMockSource = [NSString stringWithFormat: + @"选择了标签:%@\n" + "下面演示流式输出,每隔一小段时间来一截。第一段结束。\t" + "第二段开始:这是一个较长的句子,会被拆成多个小块推送,直到遇到分隔符就换段。\n第二段结束。\t" + "最后一段:右上角可随时关闭展示。\n演示完成。\t", + t]; + self.streamMockCursor = 0; + self.streamMockChunks = nil; // 废弃数组方式 + self.streamMockIndex = 0; + + // 定时喂数据(主线程):每次随机 1~3 个字 + __weak typeof(self) weakSelf = self; + self.streamMockTimer = [NSTimer scheduledTimerWithTimeInterval:0.20 repeats:YES block:^(NSTimer * _Nonnull timer) { + __strong typeof(weakSelf) self = weakSelf; if (!self) { [timer invalidate]; return; } + if (!self.streamTextView) { [timer invalidate]; return; } + NSInteger total = (NSInteger)self.streamMockSource.length; + if (self.streamMockCursor >= total) { + // 结束:收尾裁剪当前段 + [self.streamTextView finishStreaming]; + [timer invalidate]; + self.streamMockTimer = nil; + return; + } + // 随机 1~3 个字(不跨越结尾),逐段追加 + uint32_t step = arc4random_uniform(3) + 1; // 1..3 + NSInteger remain = total - self.streamMockCursor; + NSInteger take = MIN((NSInteger)step, remain); + NSString *piece = [self.streamMockSource substringWithRange:NSMakeRange(self.streamMockCursor, take)]; + self.streamMockCursor += take; + [self.streamTextView appendStreamText:piece ?: @""]; + }]; +} + +- (void)kb_stopMockStreaming { + [self.streamMockTimer invalidate]; + self.streamMockTimer = nil; + self.streamMockChunks = nil; + self.streamMockIndex = 0; + self.streamMockSource = nil; + self.streamMockCursor = 0; +} + // 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问。 // 若已开启“完全访问”,则直接在键盘侧创建 KBStreamTextView,并在其右上角提供删除按钮关闭。 - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { diff --git a/CustomKeyboard/View/KBStreamTextView.m b/CustomKeyboard/View/KBStreamTextView.m index 43647ab..156f4fa 100644 --- a/CustomKeyboard/View/KBStreamTextView.m +++ b/CustomKeyboard/View/KBStreamTextView.m @@ -8,6 +8,7 @@ // #import "KBStreamTextView.h" +#import "KBResponderUtils.h" // 通过响应链找到 UIInputViewController,并将文本输出到宿主 @interface KBStreamTextView () @@ -170,7 +171,7 @@ label.textColor = self.labelTextColor; label.userInteractionEnabled = YES; label.text = @""; - + label.backgroundColor = [UIColor redColor]; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleLabelTap:)]; [label addGestureRecognizer:tap]; @@ -256,8 +257,19 @@ UILabel *label = (UILabel *)tap.view; if (![label isKindOfClass:[UILabel class]]) { return; } NSInteger index = [self.labels indexOfObject:label]; + NSString *text = label.text ?: @""; if (index != NSNotFound && self.onLabelTap) { - self.onLabelTap(index, label.text ?: @""); + self.onLabelTap(index, text); + } + + // 将文本发送到当前宿主应用的输入框(搜索框/TextView 等) + // 注:键盘扩展无需“完全访问”也可 insertText: + UIInputViewController *ivc = KBFindInputViewController(self); + if (ivc) { + id proxy = ivc.textDocumentProxy; + if (text.length > 0 && [proxy conformsToProtocol:@protocol(UITextDocumentProxy)]) { + [proxy insertText:text]; + } } }