Files
keyboard/keyBoard/Class/Guard/VC/KBGuideVC.m
2025-11-03 19:00:47 +08:00

303 lines
12 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//
// 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 () <UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, UIGestureRecognizerDelegate>
@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<NSDictionary *> *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<NSDictionary *> *)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