// // KBSearchVC.m // keyBoard // // Created by Mac on 2025/11/7. // #import "KBSearchVC.h" #import "KBSearchBarView.h" #import "KBSearchSectionHeader.h" #import "KBTagCell.h" #import "KBSkinCardCell.h" #import "KBHistoryMoreCell.h" #import "KBSearchResultVC.h" #import "UICollectionViewLeftAlignedLayout.h" static NSString * const kTagCellId = @"KBTagCell"; static NSString * const kSkinCellId = @"KBSkinCardCell"; static NSString * const kHeaderId = @"KBSearchSectionHeader"; static NSString * const kMoreCellId = @"KBHistoryMoreCell"; static NSString * const kMoreToken = @"__KB_MORE__"; // 用于内部计算的占位标识 typedef NS_ENUM(NSInteger, KBSearchSection) { KBSearchSectionHistory = 0, KBSearchSectionRecommend = 1, }; @interface KBSearchVC () // 自定义顶部区域:返回按钮 + 搜索栏 @property (nonatomic, strong) UIView *topBar; @property (nonatomic, strong) UIButton *backButton; @property (nonatomic, strong) KBSearchBarView *searchBarView; // 列表 @property (nonatomic, strong) UICollectionView *collectionView; @property (nonatomic, strong) UICollectionViewLeftAlignedLayout *flowLayout; // 数据 @property (nonatomic, strong) NSMutableArray *historyWords; // 历史搜索 @property (nonatomic, strong) NSArray *recommendItems; // 推荐数据(title/price) @property (nonatomic, assign) BOOL historyExpanded; // 是否展开所有历史 @property (nonatomic, assign) CGFloat lastCollectionWidth; // 记录宽度变化,用于重新计算 @end @implementation KBSearchVC - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; // UI [self.view addSubview:self.topBar]; [self.topBar addSubview:self.backButton]; [self.topBar addSubview:self.searchBarView]; [self.view addSubview:self.collectionView]; // 布局 - Masonry(无导航栏) [self.topBar mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view.mas_top).offset(KB_STATUSBAR_HEIGHT + 8); make.left.right.equalTo(self.view); make.height.mas_equalTo(44); // 容器高度 }]; [self.backButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.topBar).offset(12); make.centerY.equalTo(self.topBar); make.width.mas_equalTo(28); make.height.mas_equalTo(36); }]; [self.searchBarView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self.backButton); make.left.equalTo(self.backButton.mas_right).offset(12); make.width.mas_equalTo(315); make.height.mas_equalTo(36); make.right.lessThanOrEqualTo(self.topBar).offset(-16); }]; [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.topBar.mas_bottom).offset(12); make.left.right.bottom.equalTo(self.view); }]; // 初始化测试数据 self.historyWords = [@[@"果冻橙", @"芒果", @"有机水果卷心菜", @"水果萝卜", @"熟冻帝王蟹", @"赣南脐橙"] mutableCopy]; self.recommendItems = @[ @{@"title":@"Dopamine", @"price":@"20"}, @{@"title":@"Dopamine", @"price":@"20"}, @{@"title":@"Dopamine", @"price":@"20"}, @{@"title":@"Dopamine", @"price":@"20"}, @{@"title":@"Dopamine", @"price":@"20"}, @{@"title":@"Dopamine", @"price":@"20"}, ]; [self.collectionView reloadData]; } - (void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; CGFloat w = self.collectionView.bounds.size.width; if (w > 0 && fabs(w - self.lastCollectionWidth) > 0.5) { self.lastCollectionWidth = w; [self.collectionView reloadData]; } } - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; // 隐藏系统导航栏,使用自定义返回按钮 [self.navigationController setNavigationBarHidden:YES animated:animated]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // 仅在返回上一级时恢复导航栏;push 到下一级(同样隐藏导航栏)不做处理,避免闪烁 if (self.isMovingFromParentViewController || self.isBeingDismissed) { [self.navigationController setNavigationBarHidden:NO animated:animated]; } } #pragma mark - Private /// 执行搜索:将关键字置顶到历史(去重、忽略大小写/前后空格) - (void)performSearch:(NSString *)kw { NSString *trim = [kw stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (trim.length == 0) { return; } // 忽略大小写去重(把所有等同项移除) for (NSInteger i = (NSInteger)self.historyWords.count - 1; i >= 0; i--) { NSString *old = self.historyWords[i]; if ([old caseInsensitiveCompare:trim] == NSOrderedSame) { [self.historyWords removeObjectAtIndex:i]; } } // 插入最前 [self.historyWords insertObject:trim atIndex:0]; [self.collectionView reloadData]; } /// 打开搜索结果页,把关键字传过去 - (void)openResultForKeyword:(NSString *)kw { NSString *trim = [kw stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (trim.length == 0) { return; } KBSearchResultVC *vc = [[KBSearchResultVC alloc] init]; vc.defaultKeyword = trim; [self.navigationController pushViewController:vc animated:YES]; } /// 计算“最多两行展示”的历史数据,若超出则在第二行尾部添加一个“更多”按钮(向下箭头)。 - (NSArray *)currentDisplayHistory { if (self.historyExpanded) { return self.historyWords; } if (self.collectionView.bounds.size.width <= 0) { return self.historyWords; } CGFloat width = self.collectionView.bounds.size.width; UIEdgeInsets inset = UIEdgeInsetsMake(8, 16, 12, 16); CGFloat available = width - inset.left - inset.right; CGFloat itemSpacing = 8.0; // 与代理中保持一致 NSMutableArray *display = [NSMutableArray array]; NSInteger line = 1; CGFloat lineUsed = 0.0; // 当前行已占宽度 // 记录当前第2行每个 item 的宽度与在 display 中的下标,便于回退 NSMutableArray *lineWidths = [NSMutableArray array]; NSMutableArray *lineIndexes = [NSMutableArray array]; for (NSInteger i = 0; i < self.historyWords.count; i++) { NSString *text = self.historyWords[i]; CGSize sz = [KBTagCell sizeForText:text]; CGFloat w = sz.width; CGFloat need = (lineUsed == 0 ? w : (lineUsed + itemSpacing + w)); if (need <= available) { // 放得下,加入当前行 [display addObject:text]; if (line == 2) { [lineWidths addObject:@(w)]; [lineIndexes addObject:@(display.count - 1)]; } lineUsed = need; continue; } // 放不下,需要换行或截断 if (line == 1) { // 换到第 2 行 line = 2; lineUsed = 0.0; [lineWidths removeAllObjects]; [lineIndexes removeAllObjects]; // 直接把该元素作为第 2 行的第一个 if (w <= available) { [display addObject:text]; [lineWidths addObject:@(w)]; [lineIndexes addObject:@(display.count - 1)]; lineUsed = w; continue; } else { // 极端情况:单个标签就超过一行可用宽,直接用“更多”按钮占位 [display addObject:kMoreToken]; return display; } } // 已在第 2 行且再次超出,说明超过两行。需要在第二行末尾放“更多”按钮。 CGSize moreSize = [KBHistoryMoreCell fixedSize]; CGFloat moreNeed = (lineUsed == 0 ? moreSize.width : (lineUsed + itemSpacing + moreSize.width)); if (moreNeed <= available) { [display addObject:kMoreToken]; return display; } // 放不下“更多”,从当前行尾部回退一个标签,直到能放下“更多”。 while (lineIndexes.count > 0) { NSNumber *lastIndexNum = lineIndexes.lastObject; // display 中的下标 NSNumber *lastWidthNum = lineWidths.lastObject; [display removeObjectAtIndex:lastIndexNum.integerValue]; [lineIndexes removeLastObject]; [lineWidths removeLastObject]; // 重新计算当前行已占宽 lineUsed = 0.0; for (NSInteger k = 0; k < lineWidths.count; k++) { CGFloat iw = lineWidths[k].doubleValue; lineUsed = (k == 0 ? iw : (lineUsed + itemSpacing + iw)); } // 加入更多后是否能放得下 moreNeed = (lineUsed == 0 ? moreSize.width : (lineUsed + itemSpacing + moreSize.width)); if (moreNeed <= available) { [display addObject:kMoreToken]; return display; } } // 如果全部移除仍放不下,则第二行只显示“更多” [display addObject:kMoreToken]; return display; } // 循环结束:数据未超过两行,无需“更多” return display; } /// 清空历史 - (void)clearHistory { [self.historyWords removeAllObjects]; [self.collectionView reloadData]; } #pragma mark - UICollectionViewDataSource - (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { return 2; // 历史 + 推荐 } - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { if (section == KBSearchSectionHistory) { NSArray *list = [self currentDisplayHistory]; return list.count; // 历史最多两行;可能包含“更多”占位 } return self.recommendItems.count; } - (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == KBSearchSectionHistory) { NSArray *list = [self currentDisplayHistory]; NSString *text = list[indexPath.item]; if ([text isEqualToString:kMoreToken]) { KBHistoryMoreCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kMoreCellId forIndexPath:indexPath]; return cell; } else { KBTagCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kTagCellId forIndexPath:indexPath]; [cell config:text]; return cell; } } KBSkinCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kSkinCellId forIndexPath:indexPath]; NSDictionary *it = self.recommendItems[indexPath.item]; [cell configWithTitle:it[@"title"] imageURL:nil price:it[@"price"]]; return cell; } - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { if (kind == UICollectionElementKindSectionHeader) { KBSearchSectionHeader *header = [collectionView dequeueReusableSupplementaryViewOfKind:kind withReuseIdentifier:kHeaderId forIndexPath:indexPath]; if (indexPath.section == KBSearchSectionHistory) { // 当没有历史时,外部通过 sizeForHeader 返回 0,这里仍设置但不会显示 [header configWithTitle:@"Historical Search" showTrash:self.historyWords.count > 0]; __weak typeof(self) weakSelf = self; header.onTapTrash = ^{ [weakSelf clearHistory]; }; } else { [header configWithTitle:@"Recommended Skin" showTrash:NO]; } return header; } return [UICollectionReusableView new]; } #pragma mark - UICollectionViewDelegateFlowLayout - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { CGFloat width = collectionView.bounds.size.width; if (indexPath.section == KBSearchSectionHistory) { NSArray *list = [self currentDisplayHistory]; NSString *t = list[indexPath.item]; if ([t isEqualToString:kMoreToken]) { return [KBHistoryMoreCell fixedSize]; } return [KBTagCell sizeForText:t]; } // 两列卡片 CGFloat inset = 16; // 左右间距 CGFloat spacing = 12; // 列间距 CGFloat w = floor((width - inset*2 - spacing) / 2.0); // 高度:封面 0.75w + 标题 + 价格 + 边距,近似定高 CGFloat h = w*0.75 + 8 + 20 + 10 + 6 + 8; // 估算 return CGSizeMake(w, h); } - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { if (section == KBSearchSectionHistory) { return UIEdgeInsetsMake(8, 16, 12, 16); } return UIEdgeInsetsMake(8, 16, 20, 16); } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { return section == KBSearchSectionHistory ? 8 : 12; } - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { return section == KBSearchSectionHistory ? 8 : 16; } - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { // 当历史记录为空时,header 高度为 0,从而不显示标题与垃圾桶 if (section == KBSearchSectionHistory) { return self.historyWords.count == 0 ? CGSizeZero : CGSizeMake(collectionView.bounds.size.width, 40); } return CGSizeMake(collectionView.bounds.size.width, 40); } #pragma mark - UICollectionViewDelegate - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == KBSearchSectionHistory) { NSArray *list = [self currentDisplayHistory]; NSString *kw = list[indexPath.item]; if ([kw isEqualToString:kMoreToken]) { // 展开所有历史 self.historyExpanded = YES; [self.collectionView reloadData]; } else { [self.searchBarView updateKeyword:kw]; [self performSearch:kw]; [self openResultForKeyword:kw]; } } } #pragma mark - Lazy - (KBSearchBarView *)searchBarView { if (!_searchBarView) { _searchBarView = [[KBSearchBarView alloc] init]; _searchBarView.placeholder = @"Themes"; __weak typeof(self) weakSelf = self; _searchBarView.onSearch = ^(NSString * _Nonnull keyword) { // 置顶到历史 + 打开结果页 [weakSelf performSearch:keyword]; [weakSelf openResultForKeyword:keyword]; }; } return _searchBarView; } - (UIView *)topBar { if (!_topBar) { _topBar = [[UIView alloc] init]; _topBar.backgroundColor = [UIColor whiteColor]; } return _topBar; } - (UIButton *)backButton { if (!_backButton) { _backButton = [UIButton buttonWithType:UIButtonTypeSystem]; UIImage *img = nil; if (@available(iOS 13.0, *)) { img = [UIImage systemImageNamed:@"chevron.left"]; } if (img) { [_backButton setImage:img forState:UIControlStateNormal]; } else { [_backButton setTitle:@"<" forState:UIControlStateNormal]; _backButton.titleLabel.font = [UIFont systemFontOfSize:22 weight:UIFontWeightSemibold]; } [_backButton setTintColor:[UIColor blackColor]]; [_backButton addTarget:self action:@selector(onTapBack) forControlEvents:UIControlEventTouchUpInside]; } return _backButton; } - (void)onTapBack { [self.navigationController popViewControllerAnimated:YES]; } - (UICollectionViewLeftAlignedLayout *)flowLayout { if (!_flowLayout) { _flowLayout = [[UICollectionViewLeftAlignedLayout alloc] init]; _flowLayout.scrollDirection = UICollectionViewScrollDirectionVertical; _flowLayout.sectionHeadersPinToVisibleBounds = NO; } return _flowLayout; } - (UICollectionView *)collectionView { if (!_collectionView) { _collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.flowLayout]; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.dataSource = self; _collectionView.delegate = self; // 注册 cell & header [_collectionView registerClass:KBTagCell.class forCellWithReuseIdentifier:kTagCellId]; [_collectionView registerClass:KBSkinCardCell.class forCellWithReuseIdentifier:kSkinCellId]; [_collectionView registerClass:KBHistoryMoreCell.class forCellWithReuseIdentifier:kMoreCellId]; [_collectionView registerClass:KBSearchSectionHeader.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kHeaderId]; } return _collectionView; } @end