新增搜索

This commit is contained in:
2025-12-17 20:30:01 +08:00
parent 904a6c932a
commit 1ecb7d60e5
8 changed files with 293 additions and 59 deletions

View File

@@ -0,0 +1,34 @@
//
// KBSearchThemeModel.h
// keyBoard
//
// Created by Mac on 2025/12/17.
//
#import <Foundation/Foundation.h>
#import <CoreGraphics/CoreGraphics.h>
@class KBShopThemeTagModel;
NS_ASSUME_NONNULL_BEGIN
/// /themes/search 返回的主题模型
@interface KBSearchThemeModel : NSObject
@property (nonatomic, copy, nullable) NSString *themeId;
@property (nonatomic, copy, nullable) NSString *themeName;
@property (nonatomic, assign) CGFloat themePrice;
@property (nonatomic, strong, nullable) NSArray<KBShopThemeTagModel *> *themeTag;
@property (nonatomic, copy, nullable) NSString *themeDownload;
@property (nonatomic, assign) NSInteger themeStyle;
@property (nonatomic, copy, nullable) NSString *themePreviewImageUrl;
@property (nonatomic, copy, nullable) NSString *themeDownloadUrl;
@property (nonatomic, assign) NSInteger themePurchasesNumber;
@property (nonatomic, assign) NSInteger sort;
@property (nonatomic, assign) BOOL isFree;
@property (nonatomic, assign) BOOL isPurchased;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,27 @@
//
// KBSearchThemeModel.m
// keyBoard
//
// Created by Mac on 2025/12/17.
//
#import "KBSearchThemeModel.h"
#import "KBShopThemeTagModel.h"
#import <MJExtension/MJExtension.h>
@implementation KBSearchThemeModel
+ (NSDictionary *)mj_objectClassInArray {
return @{
@"themeTag": KBShopThemeTagModel.class
};
}
+ (NSDictionary *)mj_replacedKeyFromPropertyName {
return @{
@"themeId": @"id",
};
}
@end

View File

@@ -6,6 +6,8 @@
#import "KBSearchResultVC.h"
#import "KBSearchBarView.h"
#import "KBSkinCardCell.h"
#import "KBSearchVM.h"
#import "KBSearchThemeModel.h"
static NSString * const kResultCellId = @"KBSkinCardCell";
@@ -20,8 +22,9 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) UICollectionViewFlowLayout *flowLayout;
//
@property (nonatomic, strong) NSMutableArray<NSDictionary *> *resultItems; // @{title, price}
//
@property (nonatomic, strong) NSMutableArray<KBSearchThemeModel *> *resultItems;
@property (nonatomic, strong) KBSearchVM *viewModel;
@end
@@ -66,9 +69,6 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
if (self.defaultKeyword.length > 0) {
[self.searchBarView updateKeyword:self.defaultKeyword];
[self performSearch:self.defaultKeyword];
} else {
//
[self loadMockData];
}
}
@@ -76,23 +76,22 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
#pragma mark - Private
///
///
- (void)performSearch:(NSString *)keyword {
// 10
[self.resultItems removeAllObjects];
for (int i = 0; i < 10; i++) {
[self.resultItems addObject:@{ @"title": @"Dopamine", @"price": @"20" }];
}
[self.collectionView reloadData];
}
///
- (void)loadMockData {
[self.resultItems removeAllObjects];
for (int i = 0; i < 12; i++) {
[self.resultItems addObject:@{ @"title": @"Dopamine", @"price": @"20" }];
}
[self.collectionView reloadData];
__weak typeof(self) weakSelf = self;
[self.viewModel searchThemesWithName:keyword completion:^(NSArray<KBSearchThemeModel *> * _Nullable themes, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBSearchResultVC] search failed: %@", error);
return;
}
[weakSelf.resultItems removeAllObjects];
if (themes.count > 0) {
[weakSelf.resultItems addObjectsFromArray:themes];
}
[weakSelf.collectionView reloadData];
});
}];
}
#pragma mark - UICollectionViewDataSource
@@ -107,8 +106,10 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
KBSkinCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kResultCellId forIndexPath:indexPath];
NSDictionary *it = self.resultItems[indexPath.item];
[cell configWithTitle:it[@"title"] imageURL:nil price:it[@"price"]];
KBSearchThemeModel *model = self.resultItems[indexPath.item];
[cell configWithTitle:model.themeName ?: @""
imageURL:model.themePreviewImageUrl
price:[self priceTextForTheme:model]];
return cell;
}
@@ -150,6 +151,13 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
return _searchBarView;
}
- (NSString *)priceTextForTheme:(KBSearchThemeModel *)model {
if (model.themePrice > 0.0) {
return [NSString stringWithFormat:@"%.2f", model.themePrice];
}
return @"0";
}
- (UIView *)topBar {
if (!_topBar) {
_topBar = [[UIView alloc] init];
@@ -199,11 +207,18 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
return _collectionView;
}
- (NSMutableArray<NSDictionary *> *)resultItems {
- (NSMutableArray<KBSearchThemeModel *> *)resultItems {
if (!_resultItems) {
_resultItems = [NSMutableArray array];
}
return _resultItems;
}
- (KBSearchVM *)viewModel {
if (!_viewModel) {
_viewModel = [[KBSearchVM alloc] init];
}
return _viewModel;
}
@end

View File

@@ -13,6 +13,8 @@
#import "KBHistoryMoreCell.h"
#import "KBSearchResultVC.h"
#import "UICollectionViewLeftAlignedLayout.h"
#import "KBSearchVM.h"
#import "KBShopThemeModel.h"
static NSString * const kTagCellId = @"KBTagCell";
@@ -37,9 +39,10 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
//
@property (nonatomic, strong) NSMutableArray<NSString *> *historyWords; //
@property (nonatomic, strong) NSArray<NSDictionary *> *recommendItems; // title/price
@property (nonatomic, copy) NSArray<KBShopThemeModel *> *recommendedThemes; //
@property (nonatomic, assign) BOOL historyExpanded; //
@property (nonatomic, assign) CGFloat lastCollectionWidth; //
@property (nonatomic, strong) KBSearchVM *viewModel;
@end
@implementation KBSearchVC
@@ -89,16 +92,10 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
KBLocalized(@"水果萝卜"),
KBLocalized(@"熟冻帝王蟹"),
KBLocalized(@"赣南脐橙")] 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.recommendedThemes = @[];
[self.collectionView reloadData];
[self fetchRecommendedThemes];
}
- (void)viewDidLayoutSubviews {
@@ -252,7 +249,7 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
NSArray *list = [self currentDisplayHistory];
return list.count; //
}
return self.recommendItems.count;
return self.recommendedThemes.count;
}
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
@@ -269,8 +266,10 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
}
}
KBSkinCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:kSkinCellId forIndexPath:indexPath];
NSDictionary *it = self.recommendItems[indexPath.item];
[cell configWithTitle:it[@"title"] imageURL:nil price:it[@"price"]];
KBShopThemeModel *model = self.recommendedThemes[indexPath.item];
[cell configWithTitle:model.themeName ?: @""
imageURL:model.themePreviewImageUrl
price:[self priceTextForTheme:model]];
return cell;
}
@@ -398,6 +397,28 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
- (void)onTapBack { [self.navigationController popViewControllerAnimated:YES]; }
- (void)fetchRecommendedThemes {
__weak typeof(self) weakSelf = self;
[self.viewModel fetchRecommendedThemesWithCompletion:^(NSArray<KBShopThemeModel *> * _Nullable themes, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error) {
NSLog(@"[KBSearchVC] fetch recommended failed: %@", error);
return;
}
weakSelf.recommendedThemes = themes ?: @[];
NSIndexSet *sections = [NSIndexSet indexSetWithIndex:KBSearchSectionRecommend];
[weakSelf.collectionView reloadSections:sections];
});
}];
}
- (NSString *)priceTextForTheme:(KBShopThemeModel *)model {
if (model.themePrice > 0.0) {
return [NSString stringWithFormat:@"%.2f", model.themePrice];
}
return @"0";
}
- (UICollectionViewLeftAlignedLayout *)flowLayout {
if (!_flowLayout) {
_flowLayout = [[UICollectionViewLeftAlignedLayout alloc] init];
@@ -423,4 +444,11 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
return _collectionView;
}
- (KBSearchVM *)viewModel {
if (!_viewModel) {
_viewModel = [[KBSearchVM alloc] init];
}
return _viewModel;
}
@end

View File

@@ -0,0 +1,32 @@
//
// KBSearchVM.h
// keyBoard
//
// Created by Mac on 2025/12/17.
//
#import <Foundation/Foundation.h>
@class KBShopThemeModel;
@class KBSearchThemeModel;
NS_ASSUME_NONNULL_BEGIN
typedef void(^KBSearchRecommendedCompletion)(NSArray<KBShopThemeModel *> *_Nullable themes,
NSError *_Nullable error);
typedef void(^KBSearchThemesCompletion)(NSArray<KBSearchThemeModel *> *_Nullable themes,
NSError *_Nullable error);
@interface KBSearchVM : NSObject
/// 推荐主题列表(复用 KBShopVM 的 API_THEME_RECOMMENDED
- (void)fetchRecommendedThemesWithCompletion:(KBSearchRecommendedCompletion)completion;
/// 搜索主题GET /themes/search?themeName=xxx
- (void)searchThemesWithName:(NSString *)themeName
completion:(KBSearchThemesCompletion)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,77 @@
//
// KBSearchVM.m
// keyBoard
//
// Created by Mac on 2025/12/17.
//
#import "KBSearchVM.h"
#import "KBShopVM.h"
#import "KBNetworkManager.h"
#import "KBAPI.h"
#import "KBBizCode.h"
#import "KBSearchThemeModel.h"
#import <MJExtension/MJExtension.h>
@interface KBSearchVM ()
@property (nonatomic, strong) KBShopVM *shopVM;
@end
@implementation KBSearchVM
- (NSError *)kb_invalidResponseError {
return [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidResponse
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid response")}];
}
- (NSError *)kb_invalidParameterError {
return [NSError errorWithDomain:KBNetworkErrorDomain
code:KBNetworkErrorInvalidResponse
userInfo:@{NSLocalizedDescriptionKey: KBLocalized(@"Invalid parameter")}];
}
- (void)fetchRecommendedThemesWithCompletion:(KBSearchRecommendedCompletion)completion {
[self.shopVM fetchRecommendedThemesWithCompletion:^(NSArray<KBShopThemeModel *> * _Nullable themes, NSError * _Nullable error) {
if (completion) completion(themes, error);
}];
}
- (void)searchThemesWithName:(NSString *)themeName
completion:(KBSearchThemesCompletion)completion {
NSString *trim = [themeName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
if (trim.length == 0) {
if (completion) completion(nil, [self kb_invalidParameterError]);
return;
}
NSDictionary *params = @{@"themeName": trim};
[[KBNetworkManager shared] GET:API_THEME_SEARCH
parameters:params
headers:nil
autoShowBusinessError:NO
completion:^(NSDictionary * _Nullable json,
NSURLResponse * _Nullable response,
NSError * _Nullable error) {
if (error) {
if (completion) completion(nil, error);
return;
}
id dataObj = json[KBData] ?: json[@"data"];
if (![dataObj isKindOfClass:[NSArray class]]) {
if (completion) completion(nil, [self kb_invalidResponseError]);
return;
}
NSArray<KBSearchThemeModel *> *list = [KBSearchThemeModel mj_objectArrayWithKeyValuesArray:(NSArray *)dataObj];
if (completion) completion(list, nil);
}];
}
- (KBShopVM *)shopVM {
if (!_shopVM) {
_shopVM = [[KBShopVM alloc] init];
}
return _shopVM;
}
@end