// // KBMyKeyBoardVC.m // keyBoard // // Created by Mac on 2025/11/10. // #import "KBMyKeyBoardVC.h" #import "BMLongPressDragCellCollectionView.h" #import "UICollectionViewLeftAlignedLayout.h" #import "KBMyKeyboardCell.h" #import "KBAlert.h" #import "KBMyVM.h" /// 复用标识 static NSString * const kKBMyKeyboardCellId = @"kKBMyKeyboardCellId"; /// 截图页 - 自定义键盘管理 /// 要点: /// 1)使用 BMLongPressDragCellCollectionView 支持长按拖拽排序; /// 2)cell 宽度根据文案自适应; /// 3)全部使用 Masonry 进行布局,并采用懒加载创建控件; @interface KBMyKeyBoardVC () // UI @property (nonatomic, strong) UIView *sheetView; // 底部白色容器(圆角) @property (nonatomic, strong) BMLongPressDragCellCollectionView *collectionView; // 可拖拽的列表 @property (nonatomic, strong) UIButton *saveButton; // 保存按钮 @property (nonatomic, strong) UIImageView *bgImageView; // 背景 // 数据源(必须是二维数组,库内部会在拖动时直接调整顺序) @property (nonatomic, strong) NSMutableArray *> *dataSourceArray; // {emoji,title} @property (nonatomic, strong) KBMyVM *viewModel; // 我的页面 VM @end @interface KBMyKeyBoardVC () @end @implementation KBMyKeyBoardVC - (void)viewDidLoad { [super viewDidLoad]; self.viewModel = [[KBMyVM alloc] init]; self.view.backgroundColor = [UIColor colorWithHex:0xF6F8F9]; self.kb_navView.backgroundColor = [UIColor clearColor]; self.kb_titleLabel.text = KBLocalized(@"My Keyboard"); // 布局视图 [self.view insertSubview:self.bgImageView belowSubview:self.kb_navView]; [self.view addSubview:self.sheetView]; [self.sheetView addSubview:self.collectionView]; [self.view addSubview:self.saveButton]; [self.bgImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.left.right.equalTo(self.view); make.height.mas_equalTo(323); }]; [self.sheetView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT + 60); make.bottom.equalTo(self.view).offset(16); }]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.top.equalTo(self.sheetView); make.bottom.equalTo(self.saveButton.mas_top).offset(-15); }]; [self.saveButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view).insets(UIEdgeInsetsMake(0, 24, 0, 24)); make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom).offset(-12); make.height.mas_equalTo(50); }]; // 使用后端真实数据初始化列表 [self kb_reloadUserCharacters]; } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 隐藏系统导航栏 // [self.navigationController setNavigationBarHidden:YES animated:animated]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // if (self.isMovingFromParentViewController || self.isBeingDismissed) { // [self.navigationController setNavigationBarHidden:NO animated:animated]; // } } #pragma mark - Data - (void)kb_reloadUserCharacters { __weak typeof(self) weakSelf = self; [self.viewModel fetchCharacterListByUserWithCompletion:^(NSArray * _Nonnull characterArray, NSError * _Nullable error) { // 请求失败或无数据时,不再使用本地测试数据,直接清空展示 if (error || characterArray.count == 0) { weakSelf.dataSourceArray = [NSMutableArray array]; [weakSelf.collectionView reloadData]; return; } // 将 KBCharacter 模型转换为当前列表使用的 {emoji, title, id} 结构 NSMutableArray *section = [NSMutableArray arrayWithCapacity:characterArray.count]; for (KBCharacter *c in characterArray) { NSString *emoji = c.emoji ?: @""; NSString *title = c.characterName ?: @""; NSString *identifier = c.ID ?: @""; NSString *characterId = c.characterId ?: @""; // 如果某条数据既没有 emoji 也没有标题,则忽略 if (emoji.length == 0 && title.length == 0) { continue; } NSMutableDictionary *item = [NSMutableDictionary dictionary]; item[@"emoji"] = emoji; item[@"title"] = title; item[@"characterId"] = characterId; if (identifier.length > 0) { // 用数字类型存储,便于直接作为 sort 数组上送 NSInteger cid = identifier.integerValue; item[@"id"] = @(cid); } [section addObject:item]; } weakSelf.dataSourceArray = [NSMutableArray array]; if (section.count > 0) { [weakSelf.dataSourceArray addObject:section]; } [weakSelf.collectionView reloadData]; }]; } #pragma mark - BMLongPressDragCellCollectionViewDataSource - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return self.dataSourceArray.count; } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.dataSourceArray[section].count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { KBMyKeyboardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kKBMyKeyboardCellId forIndexPath:indexPath]; NSDictionary *d = self.dataSourceArray[indexPath.section][indexPath.item]; [cell configEmoji:d[@"emoji"] title:d[@"title"]]; __weak typeof(self) weakSelf = self; __weak typeof(collectionView) weakCV = collectionView; cell.onMinusTapped = ^(KBMyKeyboardCell * _Nonnull tappedCell) { __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } NSIndexPath *tapIndexPath = [weakCV indexPathForCell:tappedCell]; if (!tapIndexPath) { return; } // 取出将要删除的人设 id NSNumber *delId = nil; // 要删除的ID NSString *characterId = @""; // 通知其他页面要刷新的关联ID if (tapIndexPath.section < self.dataSourceArray.count) { NSArray *section = self.dataSourceArray[tapIndexPath.section]; if (tapIndexPath.item < section.count) { NSDictionary *item = section[tapIndexPath.item]; id cid = item[@"id"]; characterId = item[@"characterId"]; if ([cid isKindOfClass:[NSNumber class]]) { delId = (NSNumber *)cid; } else if ([cid isKindOfClass:[NSString class]]) { NSString *cidStr = (NSString *)cid; if (cidStr.length > 0) { delId = @([cidStr integerValue]); } } } } [KBAlert confirmTitle:KBLocalized(@"Delete this tag?") message:KBLocalized(@"This action cannot be undone") ok:KBLocalized(@"Confirm") cancel:KBLocalized(@"Cancel") okColor:[UIColor redColor] cancelColor:[UIColor blackColor] completion:^(BOOL ok) { if (!ok) { return; } // 若无法获取 id,仅做本地删除以保持 UI 一致 if (!delId) { if (tapIndexPath.section < self.dataSourceArray.count) { NSMutableArray *section = self.dataSourceArray[tapIndexPath.section]; if (tapIndexPath.item < section.count) { [section removeObjectAtIndex:tapIndexPath.item]; [self.collectionView performBatchUpdates:^{ [self.collectionView deleteItemsAtIndexPaths:@[tapIndexPath]]; } completion:nil]; } } return; } // 调用删除接口,成功后刷新界面 __weak typeof(self) weakSelf2 = self; [self.viewModel deleteUserCharacterWithId:delId completion:^(BOOL success, NSError * _Nullable error) { __strong typeof(weakSelf2) strongSelf = weakSelf2; if (!strongSelf) { return; } if (!success) { NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error"); [KBHUD showInfo:msg]; return; } // 通知 App 内其他页面(如 HomeRankContentVC / HomeHotVC)该人设已被删除 NSDictionary *info = @{@"characterId": characterId ?: @0}; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:KBUserCharacterDeletedNotification object:nil userInfo:info]; }); // 重新拉取用户人设列表,刷新 UI [strongSelf kb_reloadUserCharacters]; }]; }]; }; return cell; } // 拖拽库要求实现:返回当前“二维数组”数据源 - (NSArray *> *)dataSourceWithDragCellCollectionView:(__kindof BMLongPressDragCellCollectionView *)dragCellCollectionView { return self.dataSourceArray; } // 拖拽后回调:保存最新数据 - (void)dragCellCollectionView:(BMLongPressDragCellCollectionView *)dragCellCollectionView newDataArrayAfterMove:(nullable NSArray *> *)newDataArray { self.dataSourceArray = [newDataArray mutableCopy]; } #pragma mark - BMLongPressDragCellCollectionViewDelegate (布局) // 根据文案长度动态返回 item 尺寸 - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { NSDictionary *d = self.dataSourceArray[indexPath.section][indexPath.item]; return [KBMyKeyboardCell sizeForEmoji:d[@"emoji"] title:d[@"title"]]; } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { return UIEdgeInsetsMake(12, 12, 12, 12); } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return 10; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return 10; } #pragma mark - Actions - (void)onSave { // 点击底部保存按钮时,调用更新用户人设排序接口 [self kb_updateUserCharacterSortWithShowHUD:YES]; } /// 当前 dataSourceArray 转换为接口需要的 sort 数组(按展示顺序) - (NSArray *)kb_currentSortArray { NSMutableArray *result = [NSMutableArray array]; for (NSArray *section in self.dataSourceArray) { for (NSDictionary *item in section) { id cid = item[@"id"]; if ([cid isKindOfClass:[NSNumber class]]) { [result addObject:cid]; } else if ([cid isKindOfClass:[NSString class]]) { NSString *cidStr = (NSString *)cid; if (cidStr.length > 0) { NSInteger value = cidStr.integerValue; [result addObject:@(value)]; } } } } return result; } /// 调用 VM,向后端同步用户人设排序 - (void)kb_updateUserCharacterSortWithShowHUD:(BOOL)showHUD { NSArray *sortArray = [self kb_currentSortArray]; if (showHUD) { [KBHUD show]; } __weak typeof(self) weakSelf = self; [self.viewModel updateUserCharacterSortWithSortArray:sortArray completion:^(BOOL success, NSError * _Nullable error) { __strong typeof(weakSelf) self = weakSelf; if (!self) { return; } if (showHUD) { [KBHUD dismiss]; } if (!success && error) { NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error"); [KBHUD showInfo:msg]; return; } if (showHUD) { [KBHUD showSuccess:KBLocalized(@"Saved")]; } }]; } #pragma mark - Lazy UI //- (UILabel *)titleLabel { // if (!_titleLabel) { // _titleLabel = [UILabel new]; // _titleLabel.text = @"My Keyboard"; // 顶部标题 // _titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; // _titleLabel.textColor = [UIColor colorWithHex:0x1B1F1A]; // } // return _titleLabel; //} - (UIView *)sheetView { if (!_sheetView) { _sheetView = [UIView new]; _sheetView.backgroundColor = [UIColor whiteColor]; _sheetView.layer.cornerRadius = 32.0; _sheetView.layer.masksToBounds = YES; } return _sheetView; } - (BMLongPressDragCellCollectionView *)collectionView { if (!_collectionView) { UICollectionViewLeftAlignedLayout *layout = [UICollectionViewLeftAlignedLayout new]; layout.scrollDirection = UICollectionViewScrollDirectionVertical; // layout.sectionInset = UIEdgeInsetsMake(16, 16, 16, 16); _collectionView = [[BMLongPressDragCellCollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.backgroundColor = [UIColor clearColor]; _collectionView.delegate = self; // 注意:代理为 BMLongPressDragCellCollectionViewDelegate _collectionView.dataSource = self; // 注意:数据源为 BMLongPressDragCellCollectionViewDataSource _collectionView.alwaysBounceVertical = YES; _collectionView.showsVerticalScrollIndicator = NO; [_collectionView registerClass:KBMyKeyboardCell.class forCellWithReuseIdentifier:kKBMyKeyboardCellId]; } return _collectionView; } - (UIButton *)saveButton { if (!_saveButton) { _saveButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_saveButton setTitle:KBLocalized(@"Save") forState:UIControlStateNormal]; _saveButton.titleLabel.font = [KBFont medium:16]; [_saveButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; _saveButton.backgroundColor = [UIColor colorWithHex:KBColorValue]; _saveButton.layer.cornerRadius = 25; _saveButton.layer.masksToBounds = YES; [_saveButton addTarget:self action:@selector(onSave) forControlEvents:UIControlEventTouchUpInside]; } return _saveButton; } - (UIImageView *)bgImageView{ if (!_bgImageView) { _bgImageView = [[UIImageView alloc] init]; _bgImageView.image = [UIImage imageNamed:@"my_keyboard_bg"]; } return _bgImageView; } @end