// // MySkinVC.m // keyBoard // #import "MySkinVC.h" #import "KBSkinDetailVC.h" #import #import "UIColor+Extension.h" #import #import "UIScrollView+KBEmptyView.h" // 统一空态封装(LYEmptyView) #import "MySkinCell.h" #import "KBMyVM.h" #import "KBMyTheme.h" #import "KBHUD.h" static NSString * const kMySkinCellId = @"kMySkinCellId"; @interface MySkinVC () @property (nonatomic, strong) UICollectionView *collectionView; // 列表 @property (nonatomic, strong) UIView *bottomView; // 底部操作条 @property (nonatomic, strong) UILabel *selectedLabel; // 已选择数量 @property (nonatomic, strong) UIButton *deleteButton; // 删除 @property (nonatomic, strong) NSMutableArray *data; // 已购主题 @property (nonatomic, strong) KBMyVM *viewModel; @property (nonatomic, assign, getter=isEditingMode) BOOL editingMode; // 是否编辑态 @end @implementation MySkinVC - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; // self.title = @"My Skin"; // 标题 self.kb_titleLabel.text = KBLocalized(@"My skin"); // 右上角 Editor/Cancel 使用 BaseViewController 自定义导航栏的 kb_rightButton self.kb_rightButton.hidden = NO; [self.kb_rightButton setTitle:KBLocalized(@"Editor") forState:UIControlStateNormal]; [self.kb_rightButton removeTarget:nil action:NULL forControlEvents:UIControlEventAllEvents]; [self.kb_rightButton addTarget:self action:@selector(onToggleEdit) forControlEvents:UIControlEventTouchUpInside]; // 数据源初始化为空(演示空态 + 下拉刷新) self.data = [NSMutableArray array]; // 视图 [self.view addSubview:self.collectionView]; [self.view addSubview:self.bottomView]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view).offset(KB_NAV_TOTAL_HEIGHT); make.left.right.equalTo(self.view); make.bottom.equalTo(self.bottomView.mas_top); }]; [self.bottomView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.bottom.equalTo(self.view.mas_bottom); // 初始未编辑状态:高度为 0,不占据空间 make.height.mas_equalTo(0); }]; // 空态视图(LYEmptyView)统一样式 + 重试按钮 KBWeakSelf [self.collectionView kb_makeDefaultEmptyViewWithImage:nil title:KBLocalized(@"No data") detail:KBLocalized(@"Pull down to refresh") buttonTitle:KBLocalized(@"") tapHandler:nil buttonHandler:^{ [weakSelf.collectionView.mj_header beginRefreshing]; }]; [self.collectionView kb_setLYAutoShowEnabled:NO]; // 采用手动控制显隐 // 立即按当前数据源状态显示一次空态(首屏就应展示空视图) [self.collectionView kb_endLoadingForEmpty]; // 下拉刷新(演示网络加载 + 空态切换) self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingTarget:self refreshingAction:@selector(fetchPurchasedThemes)]; // 首次进入自动刷新 [self.collectionView.mj_header beginRefreshing]; // 初始:非编辑态,隐藏底部 self.bottomView.hidden = YES; } - (void)fetchPurchasedThemes { KBWeakSelf [self.viewModel fetchPurchasedThemesWithCompletion:^(NSArray * _Nullable themes, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ if ([weakSelf.collectionView.mj_header isRefreshing]) { [weakSelf.collectionView.mj_header endRefreshing]; } if (error) { [weakSelf.collectionView kb_endLoadingForEmpty]; return; } [weakSelf.data removeAllObjects]; if (themes.count > 0) { [weakSelf.data addObjectsFromArray:themes]; } [weakSelf.collectionView reloadData]; [weakSelf.collectionView kb_endLoadingForEmpty]; }); }]; } #pragma mark - Actions - (void)onToggleEdit { self.editingMode = !self.editingMode; // 更新顶部按钮 [self.kb_rightButton setTitle:(self.isEditingMode ? KBLocalized(@"Cancel") : KBLocalized(@"Editor")) forState:UIControlStateNormal]; // 根据编辑态更新底部栏高度,并保持列表底部始终贴着 bottomView 顶部 CGFloat targetHeight = self.isEditingMode ? (KB_SAFE_BOTTOM + 44.0) : 0.0; // 展开前先确保可见,动画结束后再根据状态隐藏 self.bottomView.hidden = NO; [self.bottomView mas_updateConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(targetHeight); }]; [UIView animateWithDuration:0.25 animations:^{ [self.view layoutIfNeeded]; } completion:^(BOOL finished) { self.bottomView.hidden = !self.isEditingMode; }]; // 列表进入/退出多选 self.collectionView.allowsMultipleSelection = self.isEditingMode; // 直接刷新可见 cell,确保切换后立刻出现/隐藏 markView for (UICollectionViewCell *c in self.collectionView.visibleCells) { if (![c isKindOfClass:MySkinCell.class]) continue; MySkinCell *cell = (MySkinCell *)c; cell.editing = self.isEditingMode; NSIndexPath *ip = [self.collectionView indexPathForCell:cell]; BOOL selected = [[self.collectionView indexPathsForSelectedItems] containsObject:ip]; [cell updateSelected:selected]; } // 再做一次 reload 保守刷新(防止 offscreen cell) [self.collectionView reloadData]; // 清空选择并刷新底部文案 for (NSIndexPath *ip in [self.collectionView indexPathsForSelectedItems]) { [self.collectionView deselectItemAtIndexPath:ip animated:NO]; } [self updateBottomUI]; } - (void)onDelete { // 根据选中项删除 NSArray *selectedIndexPaths = [[self.collectionView indexPathsForSelectedItems] sortedArrayUsingSelector:@selector(compare:)]; if (selectedIndexPaths.count == 0) return; NSMutableArray *themeIds = [NSMutableArray arrayWithCapacity:selectedIndexPaths.count]; for (NSIndexPath *ip in selectedIndexPaths) { if (ip.item >= self.data.count) { continue; } KBMyTheme *theme = self.data[ip.item]; id themeIdValue = theme.themeId; NSNumber *numberId = nil; if ([themeIdValue isKindOfClass:[NSNumber class]]) { numberId = (NSNumber *)themeIdValue; } else if ([themeIdValue isKindOfClass:[NSString class]]) { NSString *idString = (NSString *)themeIdValue; if (idString.length > 0) { static NSCharacterSet *nonDigitSet; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ nonDigitSet = [[NSCharacterSet decimalDigitCharacterSet] invertedSet]; }); if ([idString rangeOfCharacterFromSet:nonDigitSet].location == NSNotFound) { numberId = @([idString longLongValue]); } } } if (numberId) { [themeIds addObject:numberId]; } } if (themeIds.count == 0) { [KBHUD showInfo:KBLocalized(@"Invalid parameter")]; return; } [KBHUD show]; KBWeakSelf [self.viewModel deletePurchasedThemesWithThemeIds:themeIds completion:^(BOOL success, NSError * _Nullable error) { dispatch_async(dispatch_get_main_queue(), ^{ [KBHUD dismiss]; if (!success) { NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error"); [KBHUD showInfo:msg]; return; } // 批量更新,先改数据,再删 UI [weakSelf.collectionView performBatchUpdates:^{ NSMutableIndexSet *set = [NSMutableIndexSet indexSet]; for (NSIndexPath *ip in selectedIndexPaths) { if (ip.item < weakSelf.data.count) { [set addIndex:ip.item]; } } [weakSelf.data removeObjectsAtIndexes:set]; [weakSelf.collectionView deleteItemsAtIndexPaths:selectedIndexPaths]; } completion:^(BOOL finished) { [weakSelf updateBottomUI]; // 根据当前数据源显隐空视图(删除到 0 条时应显示空态) [weakSelf.collectionView kb_endLoadingForEmpty]; }]; }); }]; } - (void)updateBottomUI { NSInteger count = self.collectionView.indexPathsForSelectedItems.count; NSString *format = KBLocalized(@"my_skin_selected_count"); self.selectedLabel.text = [NSString stringWithFormat:format, (long)count]; BOOL enable = count > 0; self.deleteButton.enabled = enable; if (enable) { self.deleteButton.backgroundColor = [UIColor colorWithHex:KBColorValue]; [self.deleteButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; self.deleteButton.layer.borderColor = [UIColor colorWithHex:KBColorValue].CGColor; } else { self.deleteButton.backgroundColor = [UIColor colorWithHex:0xF2F2F2]; [self.deleteButton setTitleColor:[UIColor colorWithHex:0xC8C8C8] forState:UIControlStateNormal]; self.deleteButton.layer.borderColor = [UIColor colorWithHex:0xE6E6E6].CGColor; } } #pragma mark - UICollectionView - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { return self.data.count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { MySkinCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kMySkinCellId forIndexPath:indexPath]; KBMyTheme *theme = self.data[indexPath.item]; [cell configWithTitle:theme.themeName image:nil]; cell.editing = self.isEditingMode; // 控制是否显示选择圆点 // 同步选中状态(复用时) BOOL selected = [[collectionView indexPathsForSelectedItems] containsObject:indexPath]; [cell updateSelected:selected]; return cell; } - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (!self.isEditingMode) { // 非编辑态:可在此进入详情,当前示例不处理 KBMyTheme *theme = self.data[indexPath.item]; [collectionView deselectItemAtIndexPath:indexPath animated:YES]; KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init]; vc.themeId = theme.themeId; [self.navigationController pushViewController:vc animated:true]; return; } MySkinCell *cell = (MySkinCell *)[collectionView cellForItemAtIndexPath:indexPath]; [cell updateSelected:YES]; [self updateBottomUI]; } - (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath { if (!self.isEditingMode) return; MySkinCell *cell = (MySkinCell *)[collectionView cellForItemAtIndexPath:indexPath]; [cell updateSelected:NO]; [self updateBottomUI]; } - (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { if (![cell isKindOfClass:MySkinCell.class]) return; MySkinCell *c = (MySkinCell *)cell; c.editing = self.isEditingMode; // 保证滚动出现的新 cell 同步编辑态 BOOL selected = [[collectionView indexPathsForSelectedItems] containsObject:indexPath]; [c updateSelected:selected]; } #pragma mark - Lazy - (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new]; CGFloat inset = 16; CGFloat spacing = 12; CGFloat W = UIScreen.mainScreen.bounds.size.width; CGFloat itemW = floor((W - inset * 2 - spacing) / 2.0); CGFloat itemH = KBFit(168); // 接近截图比例 layout.itemSize = CGSizeMake(itemW, itemH); layout.minimumInteritemSpacing = spacing; layout.minimumLineSpacing = spacing; layout.sectionInset = UIEdgeInsetsMake(12, inset, 12, inset); _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.allowsMultipleSelection = NO; // 初始 [_collectionView registerClass:MySkinCell.class forCellWithReuseIdentifier:kMySkinCellId]; } return _collectionView; } - (UIView *)bottomView { if (!_bottomView) { _bottomView = [UIView new]; _bottomView.backgroundColor = [UIColor whiteColor]; _bottomView.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.06].CGColor; _bottomView.layer.shadowOpacity = 1; _bottomView.layer.shadowOffset = CGSizeMake(0, -2); [_bottomView addSubview:self.selectedLabel]; [_bottomView addSubview:self.deleteButton]; [self.selectedLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(_bottomView).offset(16); // 固定在安全区内的 64 高度中居中(顶部向下 32) make.centerY.equalTo(_bottomView.mas_top).offset(32); }]; [self.deleteButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(_bottomView).offset(-16); make.centerY.equalTo(_bottomView.mas_top).offset(32); make.width.mas_equalTo(92); make.height.mas_equalTo(36); }]; } return _bottomView; } - (UILabel *)selectedLabel { if (!_selectedLabel) { _selectedLabel = [UILabel new]; _selectedLabel.textColor = [UIColor colorWithHex:KBBlackValue]; _selectedLabel.font = [KBFont medium:13]; // _selectedLabel.text = @"Selected: 0 Skins"; } return _selectedLabel; } - (UIButton *)deleteButton { if (!_deleteButton) { _deleteButton = [UIButton buttonWithType:UIButtonTypeSystem]; [_deleteButton setTitle:KBLocalized(@"Delete") forState:UIControlStateNormal]; _deleteButton.titleLabel.font = [KBFont medium:13]; _deleteButton.layer.cornerRadius = 18; _deleteButton.layer.borderWidth = 1; _deleteButton.clipsToBounds = YES; [_deleteButton addTarget:self action:@selector(onDelete) forControlEvents:UIControlEventTouchUpInside]; // 初始禁用态样式 _deleteButton.enabled = NO; _deleteButton.backgroundColor = [UIColor colorWithHex:0xF2F2F2]; [_deleteButton setTitleColor:[UIColor colorWithHex:0xC8C8C8] forState:UIControlStateNormal]; _deleteButton.layer.borderColor = [UIColor colorWithHex:0xE6E6E6].CGColor; } return _deleteButton; } - (KBMyVM *)viewModel { if (!_viewModel) { _viewModel = [[KBMyVM alloc] init]; } return _viewModel; } @end