diff --git a/keyBoard.xcodeproj/project.pbxproj b/keyBoard.xcodeproj/project.pbxproj index 4da11d3..11d8623 100644 --- a/keyBoard.xcodeproj/project.pbxproj +++ b/keyBoard.xcodeproj/project.pbxproj @@ -53,6 +53,11 @@ 047C655C2EBCD0F80035E841 /* UIView+KBShadow.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C655B2EBCD08E0035E841 /* UIView+KBShadow.m */; }; 047C655E2EBCD5B20035E841 /* UIImage+KBColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 047C655D2EBCD5B20035E841 /* UIImage+KBColor.m */; }; 048908BC2EBE1FCB00FABA60 /* BaseViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908BB2EBE1FCB00FABA60 /* BaseViewController.m */; }; + 048908C32EBE32B800FABA60 /* KBSearchVC.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908C22EBE32B800FABA60 /* KBSearchVC.m */; }; + 048908CC2EBE373500FABA60 /* KBSearchBarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908C52EBE373500FABA60 /* KBSearchBarView.m */; }; + 048908CD2EBE373500FABA60 /* KBSearchSectionHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908C72EBE373500FABA60 /* KBSearchSectionHeader.m */; }; + 048908CE2EBE373500FABA60 /* KBSkinCardCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908C92EBE373500FABA60 /* KBSkinCardCell.m */; }; + 048908CF2EBE373500FABA60 /* KBTagCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 048908CB2EBE373500FABA60 /* KBTagCell.m */; }; 04A9FE0F2EB481100020DB6D /* KBHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 04FC97082EB31B14007BD342 /* KBHUD.m */; }; 04A9FE132EB4D0D20020DB6D /* KBFullAccessManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */; }; 04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */ = {isa = PBXBuildFile; fileRef = 04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */; }; @@ -197,6 +202,16 @@ 047C655D2EBCD5B20035E841 /* UIImage+KBColor.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIImage+KBColor.m"; sourceTree = ""; }; 048908BA2EBE1FCB00FABA60 /* BaseViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BaseViewController.h; sourceTree = ""; }; 048908BB2EBE1FCB00FABA60 /* BaseViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BaseViewController.m; sourceTree = ""; }; + 048908C12EBE32B800FABA60 /* KBSearchVC.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchVC.h; sourceTree = ""; }; + 048908C22EBE32B800FABA60 /* KBSearchVC.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchVC.m; sourceTree = ""; }; + 048908C42EBE373500FABA60 /* KBSearchBarView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchBarView.h; sourceTree = ""; }; + 048908C52EBE373500FABA60 /* KBSearchBarView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchBarView.m; sourceTree = ""; }; + 048908C62EBE373500FABA60 /* KBSearchSectionHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSearchSectionHeader.h; sourceTree = ""; }; + 048908C72EBE373500FABA60 /* KBSearchSectionHeader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSearchSectionHeader.m; sourceTree = ""; }; + 048908C82EBE373500FABA60 /* KBSkinCardCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinCardCell.h; sourceTree = ""; }; + 048908C92EBE373500FABA60 /* KBSkinCardCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinCardCell.m; sourceTree = ""; }; + 048908CA2EBE373500FABA60 /* KBTagCell.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBTagCell.h; sourceTree = ""; }; + 048908CB2EBE373500FABA60 /* KBTagCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBTagCell.m; sourceTree = ""; }; 04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = ""; }; 04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = ""; }; 04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = ""; }; @@ -475,6 +490,47 @@ path = Common; sourceTree = ""; }; + 048908BD2EBE329D00FABA60 /* M */ = { + isa = PBXGroup; + children = ( + ); + path = M; + sourceTree = ""; + }; + 048908BE2EBE329D00FABA60 /* V */ = { + isa = PBXGroup; + children = ( + 048908C42EBE373500FABA60 /* KBSearchBarView.h */, + 048908C52EBE373500FABA60 /* KBSearchBarView.m */, + 048908C62EBE373500FABA60 /* KBSearchSectionHeader.h */, + 048908C72EBE373500FABA60 /* KBSearchSectionHeader.m */, + 048908C82EBE373500FABA60 /* KBSkinCardCell.h */, + 048908C92EBE373500FABA60 /* KBSkinCardCell.m */, + 048908CA2EBE373500FABA60 /* KBTagCell.h */, + 048908CB2EBE373500FABA60 /* KBTagCell.m */, + ); + path = V; + sourceTree = ""; + }; + 048908BF2EBE329D00FABA60 /* VC */ = { + isa = PBXGroup; + children = ( + 048908C12EBE32B800FABA60 /* KBSearchVC.h */, + 048908C22EBE32B800FABA60 /* KBSearchVC.m */, + ); + path = VC; + sourceTree = ""; + }; + 048908C02EBE329D00FABA60 /* Search */ = { + isa = PBXGroup; + children = ( + 048908BD2EBE329D00FABA60 /* M */, + 048908BE2EBE329D00FABA60 /* V */, + 048908BF2EBE329D00FABA60 /* VC */, + ); + path = Search; + sourceTree = ""; + }; 04A9FE122EB4D0D20020DB6D /* Manager */ = { isa = PBXGroup; children = ( @@ -699,6 +755,7 @@ 04FC95BF2EB1E3B1007BD342 /* Class */ = { isa = PBXGroup; children = ( + 048908C02EBE329D00FABA60 /* Search */, 048908B92EBDC11200FABA60 /* Common */, 04FC95B62EB1E3B1007BD342 /* Home */, 047C654D2EBCBA880035E841 /* Shop */, @@ -1184,6 +1241,7 @@ 04FC95C92EB1E4C9007BD342 /* BaseNavigationController.m in Sources */, 047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */, 047C655C2EBCD0F80035E841 /* UIView+KBShadow.m in Sources */, + 048908C32EBE32B800FABA60 /* KBSearchVC.m in Sources */, 047C655E2EBCD5B20035E841 /* UIImage+KBColor.m in Sources */, 04FC95DD2EB202A3007BD342 /* KBGuideVC.m in Sources */, 04FC95E52EB220B5007BD342 /* UIColor+Extension.m in Sources */, @@ -1202,6 +1260,10 @@ 047C65502EBCBA9E0035E841 /* KBShopVC.m in Sources */, 0477BE042EBC83130055D639 /* HomeMainVC.m in Sources */, 0477BDFD2EBC6A170055D639 /* HomeHotVC.m in Sources */, + 048908CC2EBE373500FABA60 /* KBSearchBarView.m in Sources */, + 048908CD2EBE373500FABA60 /* KBSearchSectionHeader.m in Sources */, + 048908CE2EBE373500FABA60 /* KBSkinCardCell.m in Sources */, + 048908CF2EBE373500FABA60 /* KBTagCell.m in Sources */, 0477BEA22EBCF0000055D639 /* KBTopImageButton.m in Sources */, A1B2E1012EBC7AAA00000001 /* KBTopThreeView.m in Sources */, A1B2E1022EBC7AAA00000001 /* HomeHotCell.m in Sources */, diff --git a/keyBoard/Class/Home/VC/HomeHotVC.m b/keyBoard/Class/Home/VC/HomeHotVC.m index 3b133df..bed2000 100644 --- a/keyBoard/Class/Home/VC/HomeHotVC.m +++ b/keyBoard/Class/Home/VC/HomeHotVC.m @@ -10,6 +10,7 @@ #import "KBTopThreeView.h" #import "HomeHotCell.h" #import "HomeRankVC.h" +#import "KBSearchVC.h" #import @interface HomeHotVC () @@ -80,7 +81,7 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; - HomeRankVC *vc = [[HomeRankVC alloc] init]; + KBSearchVC *vc = [[KBSearchVC alloc] init]; // [self.navigationController pushViewController:vc animated:true]; UINavigationController *nav = KB_CURRENT_NAV; [nav pushViewController:vc animated:true]; diff --git a/keyBoard/Class/Search/V/KBSearchBarView.h b/keyBoard/Class/Search/V/KBSearchBarView.h new file mode 100644 index 0000000..da1750e --- /dev/null +++ b/keyBoard/Class/Search/V/KBSearchBarView.h @@ -0,0 +1,31 @@ +// +// KBSearchBarView.h +// keyBoard +// +// 顶部搜索栏,独立封装的 View。 +// - 左侧圆角输入框,右侧搜索按钮 +// - 通过 block 将搜索事件回传给 VC +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBSearchBarView : UIView + +/// 输入框 +@property (nonatomic, strong, readonly) UITextField *textField; + +/// 点击键盘 return 或右侧按钮时回调 +@property (nonatomic, copy) void(^onSearch)(NSString *keyword); + +/// 占位文字,默认“Themes” +@property (nonatomic, copy) NSString *placeholder; + +/// 外部可设置关键字 +- (void)updateKeyword:(NSString *)keyword; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Search/V/KBSearchBarView.m b/keyBoard/Class/Search/V/KBSearchBarView.m new file mode 100644 index 0000000..3d9baf7 --- /dev/null +++ b/keyBoard/Class/Search/V/KBSearchBarView.m @@ -0,0 +1,124 @@ +// +// KBSearchBarView.m +// keyBoard +// +// 顶部搜索栏封装。 +// + +#import "KBSearchBarView.h" + +@interface KBSearchBarView () +@property (nonatomic, strong) UIView *bgView; // 圆角背景 +@property (nonatomic, strong, readwrite) UITextField *textField; // 输入框 +@property (nonatomic, strong) UIButton *searchButton; // 右侧搜索按钮 +@end + +@implementation KBSearchBarView + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self setupUI]; + } + return self; +} + +- (void)setupUI { + // 添加子视图 + [self addSubview:self.bgView]; + [self.bgView addSubview:self.textField]; + [self.bgView addSubview:self.searchButton]; + + // layout - Masonry + [self.bgView mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self); + make.height.mas_equalTo(40); + }]; + + [self.searchButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerY.equalTo(self.bgView); + make.right.equalTo(self.bgView).offset(-8); + make.width.mas_equalTo(84); + make.height.mas_equalTo(32); + }]; + + [self.textField mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.bgView).offset(12); + make.centerY.equalTo(self.bgView); + make.right.equalTo(self.searchButton.mas_left).offset(-8); + make.height.mas_equalTo(30); + }]; +} + +#pragma mark - Action + +- (void)searchButtonTapped { + NSString *kw = self.textField.text ?: @""; + if (self.onSearch) { self.onSearch(kw); } +} + +- (BOOL)textFieldShouldReturn:(UITextField *)textField { + [textField resignFirstResponder]; + if (self.onSearch) { self.onSearch(textField.text ?: @""); } + return YES; +} + +- (void)updateKeyword:(NSString *)keyword { + self.textField.text = keyword ?: @""; +} + +#pragma mark - Lazy + +- (UIView *)bgView { + if (!_bgView) { + _bgView = [[UIView alloc] init]; + // 淡灰色背景 + 圆角 + _bgView.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + _bgView.layer.cornerRadius = 20; + _bgView.layer.masksToBounds = YES; + } + return _bgView; +} + +- (UITextField *)textField { + if (!_textField) { + _textField = [[UITextField alloc] init]; + _textField.delegate = self; + _textField.font = [UIFont systemFontOfSize:15]; + _textField.textColor = [UIColor colorWithHex:0x1B1F1A]; + _textField.clearButtonMode = UITextFieldViewModeWhileEditing; + _textField.returnKeyType = UIReturnKeySearch; + _textField.placeholder = @"Themes"; // 默认占位 + + // 左侧占位图标 + UIView *pad = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 8, 8)]; + _textField.leftView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 6, 1)]; + _textField.leftViewMode = UITextFieldViewModeAlways; + (void)pad; // 保留变量避免未使用告警 + } + return _textField; +} + +- (UIButton *)searchButton { + if (!_searchButton) { + _searchButton = [UIButton buttonWithType:UIButtonTypeSystem]; + _searchButton.titleLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + [_searchButton setTitle:@"Search" forState:UIControlStateNormal]; + // 绿色样式 + [_searchButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; + _searchButton.backgroundColor = [UIColor colorWithRed:0.15 green:0.72 blue:0.62 alpha:1.0]; + _searchButton.layer.cornerRadius = 16; + _searchButton.layer.masksToBounds = YES; + [_searchButton addTarget:self action:@selector(searchButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + } + return _searchButton; +} + +#pragma mark - Public + +- (void)setPlaceholder:(NSString *)placeholder { + _placeholder = [placeholder copy]; + self.textField.placeholder = placeholder; +} + +@end + diff --git a/keyBoard/Class/Search/V/KBSearchSectionHeader.h b/keyBoard/Class/Search/V/KBSearchSectionHeader.h new file mode 100644 index 0000000..4faea23 --- /dev/null +++ b/keyBoard/Class/Search/V/KBSearchSectionHeader.h @@ -0,0 +1,29 @@ +// +// KBSearchSectionHeader.h +// keyBoard +// +// 通用区头:左侧标题,右侧可选垃圾桶按钮。 +// 用于“历史搜索”和“推荐皮肤”两个 section。 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBSearchSectionHeader : UICollectionReusableView + +/// 标题 +@property (nonatomic, strong, readonly) UILabel *titleLabel; +/// 右侧垃圾桶按钮(仅历史搜索使用) +@property (nonatomic, strong, readonly) UIButton *trashButton; + +/// 配置标题与是否显示垃圾桶 +- (void)configWithTitle:(NSString *)title showTrash:(BOOL)showTrash; + +/// 清除按钮点击回调 +@property (nonatomic, copy) void(^onTapTrash)(void); + +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Search/V/KBSearchSectionHeader.m b/keyBoard/Class/Search/V/KBSearchSectionHeader.m new file mode 100644 index 0000000..792f27f --- /dev/null +++ b/keyBoard/Class/Search/V/KBSearchSectionHeader.m @@ -0,0 +1,72 @@ +// +// KBSearchSectionHeader.m +// keyBoard +// + +#import "KBSearchSectionHeader.h" + +@interface KBSearchSectionHeader () +@property (nonatomic, strong, readwrite) UILabel *titleLabel; +@property (nonatomic, strong, readwrite) UIButton *trashButton; +@end + +@implementation KBSearchSectionHeader + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + [self setupUI]; + } + return self; +} + +- (void)setupUI { + self.backgroundColor = [UIColor whiteColor]; + + [self addSubview:self.titleLabel]; + [self addSubview:self.trashButton]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self).offset(16); + make.centerY.equalTo(self); + make.right.lessThanOrEqualTo(self.trashButton.mas_left).offset(-8); + }]; + + [self.trashButton mas_makeConstraints:^(MASConstraintMaker *make) { + make.centerY.equalTo(self); + make.right.equalTo(self).offset(-16); + make.width.height.mas_equalTo(24); + }]; +} + +- (void)tapTrash { + if (self.onTapTrash) { self.onTapTrash(); } +} + +- (void)configWithTitle:(NSString *)title showTrash:(BOOL)showTrash { + self.titleLabel.text = title; + self.trashButton.hidden = !showTrash; +} + +#pragma mark - Lazy + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.font = [UIFont systemFontOfSize:18 weight:UIFontWeightSemibold]; + _titleLabel.textColor = [UIColor colorWithHex:0x1B1F1A]; + } + return _titleLabel; +} + +- (UIButton *)trashButton { + if (!_trashButton) { + _trashButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_trashButton setImage:[UIImage systemImageNamed:@"trash"] forState:UIControlStateNormal]; + _trashButton.tintColor = [UIColor colorWithHex:0x9A9A9A]; + [_trashButton addTarget:self action:@selector(tapTrash) forControlEvents:UIControlEventTouchUpInside]; + } + return _trashButton; +} + +@end + diff --git a/keyBoard/Class/Search/V/KBSkinCardCell.h b/keyBoard/Class/Search/V/KBSkinCardCell.h new file mode 100644 index 0000000..f475e35 --- /dev/null +++ b/keyBoard/Class/Search/V/KBSkinCardCell.h @@ -0,0 +1,17 @@ +// +// KBSkinCardCell.h +// keyBoard +// +// 推荐皮肤 - 简单卡片 cell。 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBSkinCardCell : UICollectionViewCell +- (void)configWithTitle:(NSString *)title imageURL:(nullable NSString *)url price:(NSString *)price; +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Search/V/KBSkinCardCell.m b/keyBoard/Class/Search/V/KBSkinCardCell.m new file mode 100644 index 0000000..c1238b3 --- /dev/null +++ b/keyBoard/Class/Search/V/KBSkinCardCell.m @@ -0,0 +1,86 @@ +// +// KBSkinCardCell.m +// keyBoard +// + +#import "KBSkinCardCell.h" + +@interface KBSkinCardCell () +@property (nonatomic, strong) UIImageView *coverView; // 封面 +@property (nonatomic, strong) UILabel *titleLabel; // 标题 +@property (nonatomic, strong) UILabel *priceLabel; // 价格 +@end + +@implementation KBSkinCardCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.contentView.backgroundColor = [UIColor whiteColor]; + self.contentView.layer.cornerRadius = 12; + self.contentView.layer.masksToBounds = YES; + self.contentView.layer.shadowColor = [UIColor colorWithWhite:0 alpha:0.06].CGColor; + self.contentView.layer.shadowOpacity = 1; + self.contentView.layer.shadowOffset = CGSizeMake(0, 2); + + [self.contentView addSubview:self.coverView]; + [self.contentView addSubview:self.titleLabel]; + [self.contentView addSubview:self.priceLabel]; + + [self.coverView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.left.right.equalTo(self.contentView); + make.height.equalTo(self.contentView.mas_width).multipliedBy(0.75); // 4:3 + }]; + + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.contentView).offset(12); + make.right.equalTo(self.contentView).offset(-12); + make.top.equalTo(self.coverView.mas_bottom).offset(8); + }]; + + [self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.equalTo(self.titleLabel); + make.bottom.equalTo(self.contentView).offset(-10); + }]; + } + return self; +} + +- (void)configWithTitle:(NSString *)title imageURL:(NSString *)url price:(NSString *)price { + self.titleLabel.text = title.length ? title : @"Dopamine"; + self.priceLabel.text = price.length ? price : @"20"; + // 简化:本地展示占位色,无网络图 + self.coverView.backgroundColor = [UIColor colorWithWhite:0.92 alpha:1.0]; +} + +#pragma mark - Lazy + +- (UIImageView *)coverView { + if (!_coverView) { + _coverView = [[UIImageView alloc] init]; + _coverView.contentMode = UIViewContentModeScaleAspectFill; + _coverView.clipsToBounds = YES; + _coverView.backgroundColor = [UIColor colorWithWhite:0.94 alpha:1.0]; + } + return _coverView; +} + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightSemibold]; + _titleLabel.textColor = [UIColor colorWithHex:0x1B1F1A]; + } + return _titleLabel; +} + +- (UILabel *)priceLabel { + if (!_priceLabel) { + _priceLabel = [[UILabel alloc] init]; + _priceLabel.font = [UIFont systemFontOfSize:14]; + _priceLabel.textColor = [UIColor colorWithRed:0.15 green:0.72 blue:0.62 alpha:1.0]; + } + return _priceLabel; +} + +@end + diff --git a/keyBoard/Class/Search/V/KBTagCell.h b/keyBoard/Class/Search/V/KBTagCell.h new file mode 100644 index 0000000..372b864 --- /dev/null +++ b/keyBoard/Class/Search/V/KBTagCell.h @@ -0,0 +1,19 @@ +// +// KBTagCell.h +// keyBoard +// +// 历史搜索 - 标签样式 cell。 +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KBTagCell : UICollectionViewCell +- (void)config:(NSString *)text; +/// 根据文案计算自适应宽度(外部布局用) ++ (CGSize)sizeForText:(NSString *)text; +@end + +NS_ASSUME_NONNULL_END + diff --git a/keyBoard/Class/Search/V/KBTagCell.m b/keyBoard/Class/Search/V/KBTagCell.m new file mode 100644 index 0000000..2838baf --- /dev/null +++ b/keyBoard/Class/Search/V/KBTagCell.m @@ -0,0 +1,50 @@ +// +// KBTagCell.m +// keyBoard +// + +#import "KBTagCell.h" + +@interface KBTagCell () +@property (nonatomic, strong) UILabel *titleLabel; +@end + +@implementation KBTagCell + +- (instancetype)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.contentView.backgroundColor = [UIColor colorWithWhite:0.96 alpha:1.0]; + self.contentView.layer.cornerRadius = 16; + self.contentView.layer.masksToBounds = YES; + [self.contentView addSubview:self.titleLabel]; + [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { + make.edges.equalTo(self.contentView).insets(UIEdgeInsetsMake(6, 12, 6, 12)); + }]; + } + return self; +} + +- (void)config:(NSString *)text { + self.titleLabel.text = text ?: @""; +} + ++ (CGSize)sizeForText:(NSString *)text { + if (text.length == 0) { return CGSizeMake(40, 32); } + CGSize s = [text sizeWithAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:14]}]; + // 两侧内边距 12 + 12,高度固定 32 + return CGSizeMake(ceil(s.width) + 24, 32); +} + +#pragma mark - Lazy + +- (UILabel *)titleLabel { + if (!_titleLabel) { + _titleLabel = [[UILabel alloc] init]; + _titleLabel.font = [UIFont systemFontOfSize:14]; + _titleLabel.textColor = [UIColor colorWithHex:0x1B1F1A]; + } + return _titleLabel; +} + +@end + diff --git a/keyBoard/Class/Search/VC/KBSearchVC.h b/keyBoard/Class/Search/VC/KBSearchVC.h new file mode 100644 index 0000000..149f133 --- /dev/null +++ b/keyBoard/Class/Search/VC/KBSearchVC.h @@ -0,0 +1,21 @@ +// +// KBSearchVC.h +// keyBoard +// +// Created by Mac on 2025/11/7. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/// 搜索页:顶部搜索视图 + 底部集合列表 +/// 说明: +/// - 顶部搜索栏封装为单独 View(KBSearchBarView) +/// - 底部使用 `UICollectionView` 展示:历史搜索(标签流式)/ 推荐皮肤(两列卡片) +/// - 当历史记录为空时,不展示“历史搜索”标题与右侧垃圾桶 +@interface KBSearchVC : UIViewController + +@end + +NS_ASSUME_NONNULL_END diff --git a/keyBoard/Class/Search/VC/KBSearchVC.m b/keyBoard/Class/Search/VC/KBSearchVC.m new file mode 100644 index 0000000..54dabb5 --- /dev/null +++ b/keyBoard/Class/Search/VC/KBSearchVC.m @@ -0,0 +1,218 @@ +// +// 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" + +static NSString * const kTagCellId = @"KBTagCell"; +static NSString * const kSkinCellId = @"KBSkinCardCell"; +static NSString * const kHeaderId = @"KBSearchSectionHeader"; + +typedef NS_ENUM(NSInteger, KBSearchSection) { + KBSearchSectionHistory = 0, + KBSearchSectionRecommend = 1, +}; + +@interface KBSearchVC () +// 顶部搜索栏(封装的 View) +@property (nonatomic, strong) KBSearchBarView *searchBarView; +// 列表 +@property (nonatomic, strong) UICollectionView *collectionView; +@property (nonatomic, strong) UICollectionViewFlowLayout *flowLayout; + +// 数据 +@property (nonatomic, strong) NSMutableArray *historyWords; // 历史搜索 +@property (nonatomic, strong) NSArray *recommendItems; // 推荐数据(title/price) +@end + +@implementation KBSearchVC + +- (void)viewDidLoad { + [super viewDidLoad]; + self.view.backgroundColor = [UIColor whiteColor]; + + // UI + [self.view addSubview:self.searchBarView]; + [self.view addSubview:self.collectionView]; + + // 布局 - Masonry + [self.searchBarView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.view.mas_top).offset(KB_NAV_TOTAL_HEIGHT + 8); + make.left.equalTo(self.view).offset(16); + make.right.equalTo(self.view).offset(-16); + make.height.mas_equalTo(40); + }]; + + [self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.searchBarView.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]; +} + +#pragma mark - Private + +/// 执行搜索:简单将关键字加入历史 +- (void)performSearch:(NSString *)kw { + NSString *trim = [kw stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + if (trim.length == 0) { return; } + // 去重插入到最前 + [self.historyWords removeObject:trim]; + [self.historyWords insertObject:trim atIndex:0]; + [self.collectionView reloadData]; +} + +/// 清空历史 +- (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) { + return self.historyWords.count; // 无历史则为 0 + } + return self.recommendItems.count; +} + +- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + if (indexPath.section == KBSearchSectionHistory) { + KBTagCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kTagCellId forIndexPath:indexPath]; + [cell config:self.historyWords[indexPath.item]]; + 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) { + NSString *t = self.historyWords[indexPath.item]; + 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) { + NSString *kw = self.historyWords[indexPath.item]; + [self.searchBarView updateKeyword:kw]; + [self performSearch: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]; + }; + } + return _searchBarView; +} + +- (UICollectionViewFlowLayout *)flowLayout { + if (!_flowLayout) { + _flowLayout = [[UICollectionViewFlowLayout 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:KBSearchSectionHeader.class forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:kHeaderId]; + } + return _collectionView; +} + +@end