303 lines
12 KiB
Objective-C
303 lines
12 KiB
Objective-C
//
|
||
// 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
|