// // KBGuideVC.m // keyBoard // // Created by Mac on 2025/10/29. // #import "KBGuideVC.h" #import "KBGuideTopCell.h" #import "KBGuideKFCell.h" #import "KBGuideUserCell.h" #import "KBPermissionViewController.h" #import "KBKeyboardPermissionManager.h" typedef NS_ENUM(NSInteger, KBGuideItemType) { KBGuideItemTypeTop = 0, // 顶部固定卡片 KBGuideItemTypeUser, // 我方消息 KBGuideItemTypeKF // 客服回复 }; @interface KBGuideVC () @property (nonatomic, strong) BaseTableView *tableView; // 列表(继承 BaseTableView) @property (nonatomic, strong) UIView *inputBar; // 底部输入容器 @property (nonatomic, strong) UITextField *textField; // 输入框 @property (nonatomic, strong) MASConstraint *inputBarBottom;// 输入栏底部约束 @property (nonatomic, strong) UITapGestureRecognizer *bgTap;// 点击空白收起键盘 @property (nonatomic, strong) NSMutableArray *items; // 数据源 [{type, text}] /// 权限引导页作为子控制器(用于“同时隐藏”) @property (nonatomic, strong, nullable) KBPermissionViewController *permVC; @end @implementation KBGuideVC - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0]; self.title = @"使用引导"; [self.view addSubview:self.tableView]; [self.view addSubview:self.inputBar]; [self.inputBar addSubview:self.textField]; [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.equalTo(self.view); make.bottom.equalTo(self.view); }]; [self.inputBar mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.height.mas_equalTo(52); // 底部跟随键盘变化 if (@available(iOS 11.0, *)) { self.inputBarBottom = make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom); } else { self.inputBarBottom = make.bottom.equalTo(self.view); } }]; [self.textField mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.inputBar).offset(12); make.right.equalTo(self.inputBar).offset(-12); make.centerY.equalTo(self.inputBar); make.height.mas_equalTo(36); }]; // 初始只有固定 Top [self.items addObject:@{ @"type": @(KBGuideItemTypeTop), @"text": @"" }]; [self.tableView reloadData]; // 键盘监听 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_keyboardWillChange:) name:UIKeyboardWillChangeFrameNotification object:nil]; // 点击空白收起键盘(不干扰 cell 的点击/滚动) self.bgTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(kb_didTapBackground)]; self.bgTap.cancelsTouchesInView = NO; self.bgTap.delegate = self; [self.tableView addGestureRecognizer:self.bgTap]; // 监听应用回到前台/变为活跃:用于从设置返回时再次校验权限 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(kb_checkKeyboardPermission) name:UIApplicationDidBecomeActiveNotification object:nil]; // 提前创建并铺满权限引导页(默认隐藏),避免后续显示时出现布局进场感 [self kb_preparePermissionOverlayIfNeeded]; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 每次进入页面都校验一次(包括从其它页面返回) [self kb_checkKeyboardPermission]; } /// 校验键盘权限: /// - 未启用或已启用但拒绝完全访问 => 弹出引导页 /// - 已满足条件且正在展示引导页 => 关闭引导页 - (void)kb_checkKeyboardPermission { KBKeyboardPermissionManager *mgr = [KBKeyboardPermissionManager shared]; BOOL enabled = [mgr isKeyboardEnabled]; KBFARecord fa = [mgr lastKnownFullAccess]; BOOL needGuide = (!enabled) || (enabled && fa == KBFARecordDenied); [self kb_preparePermissionOverlayIfNeeded]; BOOL show = needGuide; // [UIView performWithoutAnimation:^{ self.permVC.view.hidden = !show; // }]; } /// 提前创建权限引导页覆盖层(仅一次) - (void)kb_preparePermissionOverlayIfNeeded { if (self.permVC) return; KBPermissionViewController *guide = [KBPermissionViewController new]; // guide.modalPresentationStyle = UIModalPresentationFullScreen; // 仅用于内部布局,不会真正 present KBWeakSelf; guide.backHandler = ^{ [weakSelf.navigationController popViewControllerAnimated:YES]; }; self.permVC = guide; guide.backButton.hidden = true; [self addChildViewController:guide]; [self.view addSubview:guide.view]; // [guide.view mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; [guide didMoveToParentViewController:self]; guide.view.hidden = YES; // 初始隐藏 } - (void)kb_didTapBackground { // 结束编辑,隐藏键盘 [self.view endEditing:YES]; } #pragma mark - Actions // 发送:回车发送一条消息,随后插入固定的客服回复 - (BOOL)textFieldShouldReturn:(UITextField *)textField { NSString *text = [textField.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (text.length == 0) { return NO; } // 1. 插入我方消息 [self.items addObject:@{ @"type": @(KBGuideItemTypeUser), @"text": text ?: @"" }]; // 2. 紧跟一条固定客服消息 NSString *reply = @"🎉 如您遇到其他问题,可点击在线客服帮您解决~"; [self.items addObject:@{ @"type": @(KBGuideItemTypeKF), @"text": reply }]; // 刷新并滚动到底部 [self.tableView reloadData]; [self scrollToBottomAnimated:YES]; textField.text = @""; return YES; } - (void)kb_keyboardWillChange:(NSNotification *)note { NSDictionary *info = note.userInfo; NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationOptions curve = ([info[UIKeyboardAnimationCurveUserInfoKey] integerValue] << 16); CGRect endFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue]; CGFloat screenH = UIScreen.mainScreen.bounds.size.height; CGFloat kbHeight = MAX(0, screenH - endFrame.origin.y); CGFloat safeBtm = 0; if (@available(iOS 11.0, *)) { safeBtm = self.view.safeAreaInsets.bottom; } // 输入栏距离底部 = -max(kbHeight - 安全区, 0) CGFloat offset = -MAX(kbHeight - safeBtm, 0); self.inputBarBottom.offset = offset; [UIView animateWithDuration:duration delay:0 options:curve animations:^{ UIEdgeInsets inset = self.tableView.contentInset; inset.bottom = 52 + MAX(kbHeight - safeBtm, 0); self.tableView.contentInset = inset; // self.tableView.scrollIndicatorInsets = inset; } completion:^(BOOL finished) { [self scrollToBottomAnimated:YES]; }]; } - (void)scrollToBottomAnimated:(BOOL)animated { if (self.items.count == 0) return; NSInteger last = self.items.count - 1; NSIndexPath *ip = [NSIndexPath indexPathForRow:last inSection:0]; if (last >= 0) { [self.tableView scrollToRowAtIndexPath:ip atScrollPosition:UITableViewScrollPositionBottom animated:animated]; } } #pragma mark - UITableView - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.items.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSDictionary *it = self.items[indexPath.row]; KBGuideItemType type = [it[@"type"] integerValue]; NSString *text = it[@"text"] ?: @""; if (type == KBGuideItemTypeTop) { KBGuideTopCell *cell = [tableView dequeueReusableCellWithIdentifier:[KBGuideTopCell reuseId]]; if (!cell) cell = [[KBGuideTopCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[KBGuideTopCell reuseId]]; return cell; } else if (type == KBGuideItemTypeUser) { KBGuideUserCell *cell = [tableView dequeueReusableCellWithIdentifier:[KBGuideUserCell reuseId]]; if (!cell) cell = [[KBGuideUserCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[KBGuideUserCell reuseId]]; [cell configText:text]; return cell; } else { KBGuideKFCell *cell = [tableView dequeueReusableCellWithIdentifier:[KBGuideKFCell reuseId]]; if (!cell) cell = [[KBGuideKFCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:[KBGuideKFCell reuseId]]; [cell configText:text]; return cell; } } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { return UITableViewAutomaticDimension; } - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath { return 100; } #pragma mark - Lazy - (BaseTableView *)tableView { if (!_tableView) { _tableView = [[BaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; _tableView.delegate = self; _tableView.dataSource = self; _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; // 无分割线 _tableView.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0]; _tableView.rowHeight = UITableViewAutomaticDimension; _tableView.estimatedRowHeight = 120; // 开启自适应高度 _tableView.contentInset = UIEdgeInsetsMake(0, 0, 52 + KB_SafeAreaBottom(), 0); _tableView.scrollIndicatorInsets = _tableView.contentInset; } return _tableView; } - (UIView *)inputBar { if (!_inputBar) { _inputBar = [UIView new]; _inputBar.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0]; UIView *bg = [UIView new]; bg.backgroundColor = [UIColor whiteColor]; bg.layer.cornerRadius = 10; bg.layer.masksToBounds = YES; [_inputBar addSubview:bg]; [bg mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(_inputBar).offset(12); make.right.equalTo(_inputBar).offset(-12); make.top.equalTo(_inputBar).offset(8); make.bottom.equalTo(_inputBar).offset(-8); }]; } return _inputBar; } - (UITextField *)textField { if (!_textField) { _textField = [UITextField new]; _textField.delegate = self; _textField.returnKeyType = UIReturnKeySend; // 回车发送 _textField.font = [UIFont systemFontOfSize:15]; _textField.placeholder = @"在键盘粘贴对话后,选择回复方式"; _textField.backgroundColor = [UIColor whiteColor]; _textField.layer.cornerRadius = 10; _textField.layer.masksToBounds = YES; UIView *pad = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 36)]; _textField.leftView = pad; _textField.leftViewMode = UITextFieldViewModeAlways; } return _textField; } - (NSMutableArray *)items { if (!_items) { _items = @[].mutableCopy; } return _items; } #pragma mark - UIGestureRecognizerDelegate // 避免点到输入栏触发收起 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { if (gestureRecognizer == self.bgTap) { if ([touch.view isDescendantOfView:self.inputBar]) { return NO; } } return YES; } // 与其它手势同时识别,避免影响表格滚动/选择 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return YES; } @end