2025-11-09 13:56:13 +08:00
|
|
|
|
//
|
|
|
|
|
|
// KBShopItemVC.m
|
|
|
|
|
|
// keyBoard
|
|
|
|
|
|
//
|
|
|
|
|
|
// Created by Mac on 2025/11/9.
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
|
|
#import "KBShopItemVC.h"
|
|
|
|
|
|
#import <MJRefresh/MJRefresh.h>
|
2025-11-09 18:07:47 +08:00
|
|
|
|
#import <Masonry/Masonry.h>
|
|
|
|
|
|
#import "KBSkinCardCell.h"
|
2025-11-20 14:27:57 +08:00
|
|
|
|
#import "KBSkinInstallBridge.h"
|
|
|
|
|
|
#import "KBHUD.h"
|
2025-12-11 13:16:06 +08:00
|
|
|
|
#import "KBShopVM.h"
|
2025-11-09 13:56:13 +08:00
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
@interface KBShopItemVC ()<UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
|
2025-11-09 13:56:13 +08:00
|
|
|
|
@property (nonatomic, copy) void(^scrollCallback)(UIScrollView *scrollView);
|
2025-12-11 13:16:06 +08:00
|
|
|
|
@property (nonatomic, strong) KBShopVM *internalViewModel;
|
|
|
|
|
|
@property (nonatomic, assign, getter=isLoading) BOOL loading;
|
2025-11-09 13:56:13 +08:00
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
|
|
@implementation KBShopItemVC
|
|
|
|
|
|
|
|
|
|
|
|
- (void)viewDidLoad {
|
|
|
|
|
|
[super viewDidLoad];
|
2025-11-13 15:34:56 +08:00
|
|
|
|
self.view.backgroundColor = [UIColor clearColor];
|
2025-12-11 13:16:06 +08:00
|
|
|
|
self.dataSource = [NSMutableArray array];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
// 懒加载 collectionView,并添加到视图
|
|
|
|
|
|
[self.view addSubview:self.collectionView];
|
|
|
|
|
|
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
|
|
|
|
|
|
make.edges.equalTo(self.view); // mas 布局:铺满
|
|
|
|
|
|
}];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
|
2025-11-10 15:38:30 +08:00
|
|
|
|
KBWeakSelf
|
2025-11-09 13:56:13 +08:00
|
|
|
|
if (self.isNeedHeader) {
|
2025-11-09 18:07:47 +08:00
|
|
|
|
self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
|
2025-12-11 13:16:06 +08:00
|
|
|
|
[weakSelf fetchThemes];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (self.isNeedFooter) {
|
2025-11-09 18:07:47 +08:00
|
|
|
|
self.collectionView.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
|
2025-12-11 13:16:06 +08:00
|
|
|
|
[weakSelf fetchThemes];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}];
|
2025-11-09 18:07:47 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-11 13:16:06 +08:00
|
|
|
|
self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
|
2025-11-09 13:56:13 +08:00
|
|
|
|
|
|
|
|
|
|
[self beginFirstRefresh];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
#pragma mark - 刷新控制
|
2025-11-09 13:56:13 +08:00
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
// 首次进入自动触发一次刷新(如果需要)
|
2025-11-09 13:56:13 +08:00
|
|
|
|
- (void)beginFirstRefresh {
|
2025-12-11 13:16:06 +08:00
|
|
|
|
if (self.isHeaderRefreshed || self.isLoading) {
|
|
|
|
|
|
return;
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
2025-12-11 13:16:06 +08:00
|
|
|
|
if (!self.style.styleId.length) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
[self beginRefreshImmediately];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)beginRefreshImmediately {
|
2025-12-11 13:16:06 +08:00
|
|
|
|
if (self.isNeedHeader && !self.collectionView.mj_header.isRefreshing) {
|
2025-11-09 18:07:47 +08:00
|
|
|
|
[self.collectionView.mj_header beginRefreshing];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
2025-12-11 13:16:06 +08:00
|
|
|
|
[self fetchThemes];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
- (void)fetchThemes {
|
|
|
|
|
|
if (self.isLoading) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
NSString *styleId = self.style.styleId;
|
|
|
|
|
|
if (!styleId.length) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
self.loading = YES;
|
|
|
|
|
|
KBShopVM *vm = self.shopViewModel;
|
|
|
|
|
|
if (!vm) {
|
|
|
|
|
|
if (!self.internalViewModel) {
|
|
|
|
|
|
self.internalViewModel = [[KBShopVM alloc] init];
|
|
|
|
|
|
}
|
|
|
|
|
|
vm = self.internalViewModel;
|
|
|
|
|
|
}
|
|
|
|
|
|
KBWeakSelf
|
|
|
|
|
|
[vm fetchThemesForStyleId:styleId completion:^(NSArray<KBShopThemeModel *> * _Nullable themes, NSError * _Nullable error) {
|
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
|
weakSelf.loading = NO;
|
|
|
|
|
|
weakSelf.isHeaderRefreshed = YES;
|
|
|
|
|
|
if (error) {
|
|
|
|
|
|
NSString *msg = error.localizedDescription ?: KBLocalized(@"Network error");
|
|
|
|
|
|
[KBHUD showInfo:msg];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
weakSelf.dataSource = themes.count ? themes.mutableCopy : [NSMutableArray array];
|
|
|
|
|
|
[weakSelf.collectionView reloadData];
|
|
|
|
|
|
}
|
|
|
|
|
|
if ([weakSelf.collectionView.mj_header isRefreshing]) {
|
|
|
|
|
|
[weakSelf.collectionView.mj_header endRefreshing];
|
|
|
|
|
|
}
|
|
|
|
|
|
if ([weakSelf.collectionView.mj_footer isRefreshing]) {
|
|
|
|
|
|
[weakSelf.collectionView.mj_footer endRefreshing];
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
#pragma mark - UICollectionView DataSource
|
2025-11-09 13:56:13 +08:00
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
|
|
|
|
|
if (!self.isHeaderRefreshed) return 0;
|
2025-11-09 13:56:13 +08:00
|
|
|
|
return self.dataSource.count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
- (__kindof UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
KBSkinCardCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"KBSkinCardCell" forIndexPath:indexPath];
|
2025-12-11 13:16:06 +08:00
|
|
|
|
KBShopThemeModel *theme = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : nil;
|
|
|
|
|
|
NSString *title = theme.themeName.length ? theme.themeName : KBLocalized(@"Themes");
|
|
|
|
|
|
NSString *price = [self kb_priceStringForTheme:theme];
|
|
|
|
|
|
[cell configWithTitle:title imageURL:nil price:price];
|
2025-11-09 13:56:13 +08:00
|
|
|
|
return cell;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
#pragma mark - UICollectionView DelegateFlowLayout
|
2025-11-09 13:56:13 +08:00
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
|
|
|
|
// 两列布局:左右 16 间距,中间列间距 12
|
|
|
|
|
|
CGFloat insetLR = 16.0;
|
|
|
|
|
|
CGFloat spacing = 12.0;
|
|
|
|
|
|
CGFloat contentW = collectionView.bounds.size.width - insetLR * 2;
|
|
|
|
|
|
CGFloat itemW = floor((contentW - spacing) / 2.0);
|
2025-11-17 14:53:23 +08:00
|
|
|
|
// CGFloat itemH = itemW * 0.75 + 56; // KBSkinCardCell 内部高度估算
|
|
|
|
|
|
return CGSizeMake(itemW, KBFit(197));
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section {
|
|
|
|
|
|
return UIEdgeInsetsMake(12, 16, 12, 16);
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section {
|
|
|
|
|
|
return 12.0;
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section {
|
|
|
|
|
|
return 12.0;
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 14:27:57 +08:00
|
|
|
|
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
|
|
|
|
|
|
[self kb_handleShopTapAtIndexPath:indexPath];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-11 13:16:06 +08:00
|
|
|
|
- (NSString *)kb_priceStringForTheme:(KBShopThemeModel *)theme {
|
|
|
|
|
|
if (!theme) {
|
|
|
|
|
|
return @"";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (theme.themePrice <= 0.0f) {
|
|
|
|
|
|
return KBLocalized(@"Free");
|
|
|
|
|
|
}
|
|
|
|
|
|
static NSNumberFormatter *formatter;
|
|
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
|
|
formatter = [[NSNumberFormatter alloc] init];
|
|
|
|
|
|
formatter.minimumFractionDigits = 0;
|
|
|
|
|
|
formatter.maximumFractionDigits = 2;
|
|
|
|
|
|
formatter.minimumIntegerDigits = 1;
|
|
|
|
|
|
});
|
|
|
|
|
|
NSString *priceString = [formatter stringFromNumber:@(theme.themePrice)];
|
|
|
|
|
|
return priceString ?: [NSString stringWithFormat:@"%.2f", theme.themePrice];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-20 14:27:57 +08:00
|
|
|
|
- (void)kb_handleShopTapAtIndexPath:(NSIndexPath *)indexPath {
|
2025-12-11 13:16:06 +08:00
|
|
|
|
KBShopThemeModel *theme = (indexPath.item < self.dataSource.count) ? self.dataSource[indexPath.item] : nil;
|
|
|
|
|
|
NSString *title = theme.themeName.length ? theme.themeName : KBLocalized(@"专属皮肤002");
|
2025-11-20 14:27:57 +08:00
|
|
|
|
// 将需求固定到 002.zip,本地写死皮肤 id,便于键盘扩展识别并解压。
|
2025-11-20 18:23:56 +08:00
|
|
|
|
static NSString * const kKBBundleSkinId002 = @"bundle_skin_fense";
|
2025-11-20 14:27:57 +08:00
|
|
|
|
[KBSkinInstallBridge publishBundleSkinRequestWithId:kKBBundleSkinId002
|
|
|
|
|
|
name:title ?: kKBBundleSkinId002
|
2025-11-20 18:23:56 +08:00
|
|
|
|
zipName:@"fense.zip"
|
2025-11-20 14:27:57 +08:00
|
|
|
|
iconShortNames:nil];
|
|
|
|
|
|
[KBHUD showInfo:KBLocalized(@"已通知键盘解压,切换到自定义键盘即可生效")];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-11 13:16:06 +08:00
|
|
|
|
- (void)setStyle:(KBShopStyleModel *)style {
|
|
|
|
|
|
_style = style;
|
|
|
|
|
|
if (self.isViewLoaded && !self.isHeaderRefreshed) {
|
|
|
|
|
|
[self beginFirstRefresh];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
#pragma mark - UIScrollView Delegate(转发给分页容器)
|
|
|
|
|
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
|
|
|
|
|
!self.scrollCallback ?: self.scrollCallback(scrollView);
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-09 18:07:47 +08:00
|
|
|
|
#pragma mark - JXPagingViewListViewDelegate
|
|
|
|
|
|
- (UIView *)listView { return self.view; }
|
|
|
|
|
|
- (UIScrollView *)listScrollView { return self.collectionView; }
|
|
|
|
|
|
- (void)listViewDidScrollCallback:(void (^)(UIScrollView *))callback { self.scrollCallback = callback; }
|
|
|
|
|
|
- (void)listWillAppear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); }
|
|
|
|
|
|
- (void)listDidAppear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); }
|
|
|
|
|
|
- (void)listWillDisappear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); }
|
|
|
|
|
|
- (void)listDidDisappear { NSLog(@"%@:%@", self.title, NSStringFromSelector(_cmd)); }
|
|
|
|
|
|
|
|
|
|
|
|
#pragma mark - Lazy
|
|
|
|
|
|
- (UICollectionView *)collectionView {
|
|
|
|
|
|
if (!_collectionView) {
|
|
|
|
|
|
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
|
|
|
|
|
|
layout.scrollDirection = UICollectionViewScrollDirectionVertical;
|
|
|
|
|
|
_collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
|
2025-11-13 15:34:56 +08:00
|
|
|
|
_collectionView.backgroundColor = [UIColor clearColor];
|
2025-11-09 18:07:47 +08:00
|
|
|
|
_collectionView.dataSource = self;
|
|
|
|
|
|
_collectionView.delegate = self;
|
|
|
|
|
|
// 注册皮肤卡片 cell
|
|
|
|
|
|
[_collectionView registerClass:KBSkinCardCell.class forCellWithReuseIdentifier:@"KBSkinCardCell"]; // 复用标识
|
|
|
|
|
|
}
|
|
|
|
|
|
return _collectionView;
|
2025-11-09 13:56:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@end
|