Files
keyboard/keyBoard/Class/Search/VC/KBSearchVC.m
2025-11-10 15:38:30 +08:00

430 lines
17 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.

//
// 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 ()<UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
// 自定义顶部区域:返回按钮 + 搜索栏
@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<NSString *> *historyWords; // 历史搜索
@property (nonatomic, strong) NSArray<NSDictionary *> *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<NSString *> *)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<NSString *> *display = [NSMutableArray array];
NSInteger line = 1;
CGFloat lineUsed = 0.0; // 当前行已占宽度
// 记录当前第2行每个 item 的宽度与在 display 中的下标,便于回退
NSMutableArray<NSNumber *> *lineWidths = [NSMutableArray array];
NSMutableArray<NSNumber *> *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];
KBWeakSelf
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";
KBWeakSelf
_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