// // KBFunctionView.m // CustomKeyboard // // Created by Mac on 2025/10/28. // #import "KBFunctionView.h" #import "KBResponderUtils.h" // 统一查找 UIInputViewController 的工具 #import "KBFunctionBarView.h" #import "KBFunctionPasteView.h" #import "KBFunctionTagCell.h" #import "Masonry.h" #import #import "KBFullAccessGuideView.h" #import "KBFullAccessManager.h" #import "KBSkinManager.h" #import "KBURLOpenBridge.h" // 兜底从扩展侧直接尝试 openURL: #import "KBStreamTextView.h" // 流式文本视图 #import "KBStreamOverlayView.h" // 带关闭按钮的流式层 #import "KBFunctionTagListView.h" #import "KBStreamFetcher.h" // 网络流封装 static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId"; @interface KBFunctionView () // UI @property (nonatomic, strong) KBFunctionBarView *barViewInternal; @property (nonatomic, strong) KBFunctionPasteView *pasteViewInternal; @property (nonatomic, strong) KBFunctionTagListView *tagListView; @property (nonatomic, strong) UIView *rightButtonContainer; // 右侧竖排按钮容器 @property (nonatomic, strong) UIButton *pasteButtonInternal; @property (nonatomic, strong) UIButton *deleteButtonInternal; @property (nonatomic, strong) UIButton *clearButtonInternal; @property (nonatomic, strong) UIButton *sendButtonInternal; // 叠层:流式文本视图 + 关闭按钮 @property (nonatomic, strong, nullable) KBStreamOverlayView *streamOverlay; // 网络流式(封装) @property (nonatomic, strong, nullable) KBStreamFetcher *streamFetcher; @property (nonatomic, assign) BOOL streamHasOutput; // 是否已输出过正文(首段去首个 \t 用) // Data @property (nonatomic, strong) NSArray *itemsInternal; // 剪贴板自动检测 @property (nonatomic, strong) NSTimer *pasteboardTimer; // 轮询定时器(轻量、主线程) @property (nonatomic, assign) NSInteger lastHandledPBCount; // 上次处理过的 changeCount,避免重复弹窗 @end @implementation KBFunctionView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 背景使用当前主题强调色 [self kb_applyTheme]; [self setupUI]; [self reloadDemoData]; // 初始化剪贴板监控状态 _lastHandledPBCount = [UIPasteboard generalPasteboard].changeCount; // 监听“完全访问”状态变化,动态启停剪贴板监控,避免在未开完全访问时触发 TCC/XPC 错误日志 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_fullAccessChanged) name:KBFullAccessChangedNotification object:nil]; } return self; } #pragma mark - Theme - (void)kb_applyTheme { KBSkinManager *mgr = [KBSkinManager shared]; UIColor *accent = mgr.current.accentColor ?: [UIColor colorWithRed:0.77 green:0.93 blue:0.82 alpha:1.0]; BOOL hasImg = ([mgr currentBackgroundImage] != nil); self.backgroundColor = hasImg ? [accent colorWithAlphaComponent:0.65] : accent; } - (void)dealloc { [self stopPasteboardMonitor]; [self kb_stopNetworkStreaming]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - UI - (void)setupUI { // 1. 顶部 Bar [self addSubview:self.barViewInternal]; [self.barViewInternal mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self); make.top.equalTo(self.mas_top).offset(6); make.height.mas_equalTo(48); }]; // 右侧竖排按钮容器 [self addSubview:self.rightButtonContainer]; [self.rightButtonContainer mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.mas_right).offset(-12); make.top.equalTo(self.barViewInternal.mas_bottom).offset(8); make.bottom.equalTo(self.mas_bottom).offset(-10); make.width.mas_equalTo(72); }]; // 右侧四个按钮 [self.rightButtonContainer addSubview:self.pasteButtonInternal]; [self.rightButtonContainer addSubview:self.deleteButtonInternal]; [self.rightButtonContainer addSubview:self.clearButtonInternal]; [self.rightButtonContainer addSubview:self.sendButtonInternal]; // 竖向排布:粘贴、删除、清空为等高;发送优先更高,但允许在空间不足时压缩 CGFloat smallH = 44; CGFloat bigH = 56; // 原 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); make.height.mas_equalTo(smallH); }]; [self.deleteButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.pasteButtonInternal.mas_bottom).offset(vSpace); make.left.right.equalTo(self.rightButtonContainer); make.height.equalTo(self.pasteButtonInternal); }]; [self.clearButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.deleteButtonInternal.mas_bottom).offset(vSpace); make.left.right.equalTo(self.rightButtonContainer); make.height.equalTo(self.pasteButtonInternal); }]; [self.sendButtonInternal mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.clearButtonInternal.mas_bottom).offset(vSpace); make.left.right.equalTo(self.rightButtonContainer); // 允许在空间不足时缩短到 smallH,避免产生约束冲突 make.height.greaterThanOrEqualTo(@(smallH)); make.height.lessThanOrEqualTo(@(bigH)); make.bottom.lessThanOrEqualTo(self.rightButtonContainer.mas_bottom); }]; // 2. 粘贴区(位于右侧按钮左侧) [self addSubview:self.pasteViewInternal]; [self.pasteViewInternal mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.mas_left).offset(12); make.right.equalTo(self.rightButtonContainer.mas_left).offset(-12); make.top.equalTo(self.barViewInternal.mas_bottom).offset(8); make.height.mas_equalTo(48); }]; // 3. Tag List View [self addSubview:self.tagListView]; [self.tagListView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.mas_left).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); }]; } #pragma mark - Data - (void)reloadDemoData { // 演示数据(可由外部替换) self.itemsInternal = @[@"高情商", @"暖味拉扯", @"风趣幽默", @"撩女生", @"社交惬匿", @"情场高手", @"一枚暖男", @"聊天搭子", @"表达爱意", @"更多话术"]; [self.tagListView setItems:self.itemsInternal]; } #pragma mark - UICollectionView - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.itemsInternal.count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBFunctionTagCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBFunctionTagCellId forIndexPath:indexPath]; cell.titleLabel.text = self.itemsInternal[indexPath.item]; return cell; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { // 三列等宽 CGFloat totalW = collectionView.bounds.size.width; CGFloat space = 10.0; NSInteger columns = 3; CGFloat width = floor((totalW - space * (columns - 1)) / columns); return CGSizeMake(width, 48); } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return 10.0; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 12.0; } - (void)kb_showStreamTextViewIfNeededWithTitle:(NSString *)title { // 已有则不重复创建 if (self.streamOverlay.superview) { return; } // 隐藏标签列表,使用同一区域展示流式文本 self.tagListView.hidden = YES; KBStreamOverlayView *overlay = [[KBStreamOverlayView alloc] init]; overlay.delegate = (id)self; [self addSubview:overlay]; [overlay mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.mas_left).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); }]; self.streamOverlay = overlay; // 优先拉取后端测试数据(GET);失败则回落到本地演示 [self kb_startNetworkStreamingWithSeed:title]; } - (void)kb_onTapStreamDelete { // 关闭并销毁流式视图,恢复标签列表 [self kb_stopNetworkStreaming]; [self.streamOverlay removeFromSuperview]; self.streamOverlay = nil; self.tagListView.hidden = NO; } // 叠层关闭回调 - (void)streamOverlayDidTapClose:(KBStreamOverlayView *)overlay { [self kb_onTapStreamDelete]; } #pragma mark - Network Streaming (GET) // 后端测试地址(内网)。若被 ATS 拦截,请在扩展 target 的 Info.plist 中允许 HTTP 或添加例外域。 static NSString * const kKBStreamDemoURL = @"http://192.168.1.144:7529/api/demo/talk"; - (void)kb_startNetworkStreamingWithSeed:(NSString *)seedTitle { [self kb_stopNetworkStreaming]; if (![[KBFullAccessManager shared] hasFullAccess]) { return; } NSURL *url = [NSURL URLWithString:kKBStreamDemoURL]; if (!url) { return; } self.streamHasOutput = NO; // 重置首段处理标记 __weak typeof(self) weakSelf = self; KBStreamFetcher *fetcher = [KBStreamFetcher fetcherWithURL:url]; // 由本类统一做 /t->\t 与首段去 \t,fetcher 只负责增量与协议解析 fetcher.disableCompression = YES; fetcher.acceptEventStream = NO; // 响应头若是 SSE 仍会自动解析 // 将 \t 与首段去 \t 的处理下沉到 Fetcher,避免 UI 抖动 fetcher.treatSlashTAsTab = YES; fetcher.trimLeadingTabOnce = YES; fetcher.flushInterval = 0.05; // 更接近“立刻显示”的节奏 fetcher.onChunk = ^(NSString *chunk) { __strong typeof(weakSelf) self = weakSelf; if (!self) return; [self kb_appendChunkToStreamView:chunk]; }; fetcher.onFinish = ^(NSError * _Nullable error) { __strong typeof(weakSelf) self = weakSelf; if (!self) return; if (error) { [KBHUD showInfo:@"拉取失败"]; } [self.streamOverlay finish]; }; self.streamFetcher = fetcher; [self.streamFetcher start]; } - (void)kb_stopNetworkStreaming { [self.streamFetcher cancel]; self.streamFetcher = nil; self.streamHasOutput = NO; } #pragma mark - Helpers /// 统一处理需要输出到 KBStreamTextView 的分片: /// - 目前网络层(KBStreamFetcher)已做 “/t->\t、首段去一个 \t、段间去一个空格” /// - 这里仅负责附加到视图与标记首段状态,避免 UI 抖动 - (void)kb_appendChunkToStreamView:(NSString *)chunk { if (chunk.length == 0 || !self.streamOverlay) return; [self.streamOverlay appendChunk:chunk]; self.streamHasOutput = YES; } #pragma mark - KBFunctionTagListViewDelegate - (void)tagListView:(KBFunctionTagListView *)view didSelectIndex:(NSInteger)index title:(NSString *)title { // 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。 if ([[KBFullAccessManager shared] hasFullAccess]) { [self kb_showStreamTextViewIfNeededWithTitle:title ?: @""]; return; } // 保留原有拉起主 App 的逻辑(若需要) [KBHUD showInfo:@"处理中…"]; UIInputViewController *ivc = KBFindInputViewController(self); if (!ivc) return; NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @""; NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)index, encodedTitle]]; if (!ul) return; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) { if (ok) return; // Universal Link 成功 NSURL *scheme = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=functionView&index=%ld&title=%@", KB_APP_SCHEME, (long)index, encodedTitle]]; [ivc.extensionContext openURL:scheme completionHandler:^(BOOL ok2) { if (ok2) return; 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) { dispatch_async(dispatch_get_main_queue(), ^{ [[KBFullAccessManager shared] ensureFullAccessOrGuideInView:self]; }); } }]; }]; }); } // 用户点击功能标签:优先 UL 拉起主App,失败再 Scheme;两次都失败则提示开启完全访问。 // 若已开启“完全访问”,则直接在键盘侧创建 KBStreamTextView,并在其右上角提供删除按钮关闭。 - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { // 权限全部打开(键盘已启用 + 完全访问)。在扩展进程中仅需判断“完全访问”。 if ([[KBFullAccessManager shared] hasFullAccess]) { NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @""; [self kb_showStreamTextViewIfNeededWithTitle:title]; return; } [KBHUD showInfo:@"处理中…"]; UIInputViewController *ivc = KBFindInputViewController(self); if (!ivc) return; NSString *title = (indexPath.item < self.itemsInternal.count) ? self.itemsInternal[indexPath.item] : @""; NSString *encodedTitle = [title stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]] ?: @""; NSURL *ul = [NSURL URLWithString:[NSString stringWithFormat:@"%@?src=functionView&index=%ld&title=%@", KB_UL_LOGIN, (long)indexPath.item, encodedTitle]]; if (!ul) return; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [ivc.extensionContext openURL:ul completionHandler:^(BOOL ok) { if (ok) return; // Universal Link 成功 // 统一使用主 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) 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]; }); } }]; }]; }); } #pragma mark - Button Actions - (void)onTapPaste { // 用户点击“粘贴”时才读取剪贴板: // - iOS16+ 会在跨 App 首次读取时自动弹出系统权限弹窗; // - iOS15 及以下不会弹窗,直接返回内容; // 注意:不要在非用户触发的时机主动读取(如 viewDidLoad),否则会造成“立刻弹窗”的体验。 UIPasteboard *pb = [UIPasteboard generalPasteboard]; NSString *text = pb.string; // 读取纯文本(可能触发系统粘贴权限弹窗) if (text.length > 0) { // 将粘贴内容展示到左侧“粘贴区”的占位文案上 self.pasteView.placeholderLabel.text = text; // 如果需要多行展示,可按需放开(高度由外部约束决定,默认一行会截断) // self.pasteView.placeholderLabel.numberOfLines = 0; } else { // 无可用文本或用户拒绝了粘贴权限;保持占位文案不变 NSLog(@"粘贴板无可用文本或未授权粘贴"); } } #pragma mark - 自动监控剪贴板(复制即弹窗) // 说明: // - 仅在视图可见时开启轮询,避免不必要的读取与打扰; // - 当检测到 changeCount 变化,立即读 pasteboard.string: // * iOS16+:此处会触发系统“是否允许粘贴”弹窗; // * iOS15:不会弹窗,直接得到文本; // - 无论允许/拒绝,都把本次 changeCount 记为已处理,避免一直重复询问。 - (void)startPasteboardMonitor { // 未开启“完全访问”时不做自动读取,避免宿主/系统拒绝并刷错误日志 if (![[KBFullAccessManager shared] hasFullAccess]) return; if (self.pasteboardTimer) return; KBWeakSelf self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) { __strong typeof(weakSelf) self = weakSelf; if (!self) return; UIPasteboard *pb = [UIPasteboard generalPasteboard]; NSInteger cc = pb.changeCount; if (cc <= self.lastHandledPBCount) return; // 没有新复制 self.lastHandledPBCount = cc; // 标记已处理,避免重复 // 实际读取触发系统弹窗(iOS16+) NSString *text = pb.string; if (text.length > 0) { self.pasteView.placeholderLabel.text = text; } }]; } - (void)stopPasteboardMonitor { [self.pasteboardTimer invalidate]; self.pasteboardTimer = nil; } - (void)didMoveToWindow { [super didMoveToWindow]; [self kb_refreshPasteboardMonitor]; } - (void)setHidden:(BOOL)hidden { BOOL wasHidden = self.isHidden; [super setHidden:hidden]; if (wasHidden != hidden) { [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); id proxy = ivc.textDocumentProxy; [proxy deleteBackward]; } - (void)onTapClear { NSLog(@"点击:清空"); // 连续删除:仅清空光标之前的输入(不改动 pasteView 的内容) UIInputViewController *ivc = KBFindInputViewController(self); id proxy = ivc.textDocumentProxy; // 逐批读取 documentContextBeforeInput 并删除,避免 50 字符窗口限制带来的残留 NSInteger guard = 0; // 上限保护,避免极端情况下长时间阻塞 while (guard < 10000) { NSString *before = proxy.documentContextBeforeInput ?: @""; NSInteger count = before.length; if (count <= 0) { break; } // 光标前已无内容 for (NSInteger i = 0; i < count; i++) { [proxy deleteBackward]; } guard += count; } } - (void)onTapSend { NSLog(@"点击:发送"); // 发送:插入换行。大多数聊天类 App 会把回车视为“发送” UIInputViewController *ivc = KBFindInputViewController(self); id proxy = ivc.textDocumentProxy; [proxy insertText:@"\n"]; } #pragma mark - Lazy - (KBFunctionBarView *)barViewInternal { if (!_barViewInternal) { _barViewInternal = [[KBFunctionBarView alloc] init]; _barViewInternal.delegate = self; // 顶部功能Bar事件下发到本View } return _barViewInternal; } #pragma mark - KBFunctionBarViewDelegate - (void)functionBarView:(KBFunctionBarView *)bar didTapLeftAtIndex:(NSInteger)index { // 将事件继续透传给上层(如键盘控制器),用于切换界面或其它业务 if ([self.delegate respondsToSelector:@selector(functionView:didTapToolActionAtIndex:)]) { [self.delegate functionView:self didTapToolActionAtIndex:index]; } } - (void)functionBarView:(KBFunctionBarView *)bar didTapRightAtIndex:(NSInteger)index { // 右侧按钮点击,如收藏/宫格等,按需继续向外抛出(这里暂不定义单独协议方法,可在此内部处理或扩展) } - (KBFunctionPasteView *)pasteViewInternal { if (!_pasteViewInternal) { _pasteViewInternal = [[KBFunctionPasteView alloc] init]; } return _pasteViewInternal; } - (KBFunctionTagListView *)tagListView { if (!_tagListView) { _tagListView = [[KBFunctionTagListView alloc] init]; _tagListView.delegate = (id)self; } return _tagListView; } - (UIView *)rightButtonContainer { if (!_rightButtonContainer) { _rightButtonContainer = [[UIView alloc] init]; _rightButtonContainer.backgroundColor = [UIColor clearColor]; } return _rightButtonContainer; } - (UIButton *)buildRightButtonWithTitle:(NSString *)title color:(UIColor *)color { UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem]; btn.backgroundColor = color; btn.layer.cornerRadius = 12.0; btn.layer.masksToBounds = YES; btn.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; [btn setTitle:title forState:UIControlStateNormal]; [btn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; return btn; } - (UIButton *)pasteButtonInternal { if (!_pasteButtonInternal) { _pasteButtonInternal = [self buildRightButtonWithTitle:@"粘贴" color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]]; [_pasteButtonInternal addTarget:self action:@selector(onTapPaste) forControlEvents:UIControlEventTouchUpInside]; } return _pasteButtonInternal; } - (UIButton *)deleteButtonInternal { if (!_deleteButtonInternal) { // 浅灰底深色文字,更接近截图里“删除”样式 _deleteButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem]; _deleteButtonInternal.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0]; _deleteButtonInternal.layer.cornerRadius = 12.0; _deleteButtonInternal.layer.masksToBounds = YES; _deleteButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; [_deleteButtonInternal setTitle:@"删除" forState:UIControlStateNormal]; [_deleteButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; [_deleteButtonInternal addTarget:self action:@selector(onTapDelete) forControlEvents:UIControlEventTouchUpInside]; } return _deleteButtonInternal; } - (UIButton *)clearButtonInternal { if (!_clearButtonInternal) { _clearButtonInternal = [UIButton buttonWithType:UIButtonTypeSystem]; _clearButtonInternal.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0]; _clearButtonInternal.layer.cornerRadius = 12.0; _clearButtonInternal.layer.masksToBounds = YES; _clearButtonInternal.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; [_clearButtonInternal setTitle:@"清空" forState:UIControlStateNormal]; [_clearButtonInternal setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; [_clearButtonInternal addTarget:self action:@selector(onTapClear) forControlEvents:UIControlEventTouchUpInside]; } return _clearButtonInternal; } - (UIButton *)sendButtonInternal { if (!_sendButtonInternal) { _sendButtonInternal = [self buildRightButtonWithTitle:@"发送" color:[UIColor colorWithRed:0.13 green:0.73 blue:0.60 alpha:1.0]]; [_sendButtonInternal addTarget:self action:@selector(onTapSend) forControlEvents:UIControlEventTouchUpInside]; } return _sendButtonInternal; } #pragma mark - Expose - (UICollectionView *)collectionView { return self.tagListView.collectionView; } - (NSArray *)items { return self.itemsInternal; } - (KBFunctionBarView *)barView { return self.barViewInternal; } - (KBFunctionPasteView *)pasteView { return self.pasteViewInternal; } - (UIButton *)pasteButton { return self.pasteButtonInternal; } - (UIButton *)deleteButton { return self.deleteButtonInternal; } - (UIButton *)clearButton { return self.clearButtonInternal; } - (UIButton *)sendButton { return self.sendButtonInternal; } #pragma mark - Find Owner Controller // 工具方法已提取到 KBResponderUtils.h @end