Compare commits

...

4 Commits

Author SHA1 Message Date
8069b08fab Merge branch 'dev_st'
# Conflicts:
#	keyBoard.xcodeproj/project.pbxproj
解决冲突
2025-11-10 16:12:38 +08:00
9f4110b24a 添加弹窗 2025-11-10 15:55:36 +08:00
1cdc17b710 1 2025-11-10 15:38:30 +08:00
97316c7989 添加底部view 2025-11-10 15:29:21 +08:00
25 changed files with 630 additions and 32 deletions

View File

@@ -234,7 +234,7 @@ static CGFloat KEYBOARDHEIGHT = 256 + 20;
// 使 App Scheme
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"%@@//login?src=keyboard", KB_APP_SCHEME]];
if (!url) return;
__weak typeof(self) weakSelf = self;
KBWeakSelf
[self.extensionContext openURL:url completionHandler:^(__unused BOOL success) {
// 使
__unused typeof(weakSelf) selfStrong = weakSelf;

View File

@@ -259,7 +259,7 @@ static NSString * const kKBFunctionTagCellId = @"KBFunctionTagCellId";
// 访宿/
if (![[KBFullAccessManager shared] hasFullAccess]) return;
if (self.pasteboardTimer) return;
__weak typeof(self) weakSelf = self;
KBWeakSelf
self.pasteboardTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:YES block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
UIPasteboard *pb = [UIPasteboard generalPasteboard];

View File

@@ -338,7 +338,6 @@
if (before.length <= 0) { self.backspaceHoldActive = NO; return; }
[proxy deleteBackward]; // 1
// 使 NSTimer
__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.06 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
__strong typeof(weakSelf) selfStrong = weakSelf;

View File

@@ -75,6 +75,8 @@
048909F72EC0AAAA00FABA60 /* KBCategoryTitleView.m in Sources */ = {isa = PBXBuildFile; fileRef = 048909F32EC0AAAA00FABA60 /* KBCategoryTitleView.m */; };
04890A042EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890A012EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m */; };
04890A052EC0BBBB00FABA60 /* KBCategoryTitleImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 04890A032EC0BBBB00FABA60 /* KBCategoryTitleImageView.m */; };
049FB20B2EC1C13800FAB05D /* KBSkinBottomActionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB20A2EC1C13800FAB05D /* KBSkinBottomActionView.m */; };
049FB20E2EC1CD2800FAB05D /* KBAlert.m in Sources */ = {isa = PBXBuildFile; fileRef = 049FB20D2EC1CD2800FAB05D /* KBAlert.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 */; };
@@ -264,6 +266,10 @@
04890A012EC0BBBB00FABA60 /* KBCategoryTitleImageCell.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBCategoryTitleImageCell.m; sourceTree = "<group>"; };
04890A022EC0BBBB00FABA60 /* KBCategoryTitleImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBCategoryTitleImageView.h; sourceTree = "<group>"; };
04890A032EC0BBBB00FABA60 /* KBCategoryTitleImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBCategoryTitleImageView.m; sourceTree = "<group>"; };
049FB2092EC1C13800FAB05D /* KBSkinBottomActionView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBSkinBottomActionView.h; sourceTree = "<group>"; };
049FB20A2EC1C13800FAB05D /* KBSkinBottomActionView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBSkinBottomActionView.m; sourceTree = "<group>"; };
049FB20C2EC1CD2800FAB05D /* KBAlert.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBAlert.h; sourceTree = "<group>"; };
049FB20D2EC1CD2800FAB05D /* KBAlert.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBAlert.m; sourceTree = "<group>"; };
04A9A67D2EB9E1690023B8F4 /* KBResponderUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBResponderUtils.h; sourceTree = "<group>"; };
04A9FE102EB4D0D20020DB6D /* KBFullAccessManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KBFullAccessManager.h; sourceTree = "<group>"; };
04A9FE112EB4D0D20020DB6D /* KBFullAccessManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KBFullAccessManager.m; sourceTree = "<group>"; };
@@ -544,6 +550,10 @@
048908B82EBDC11200FABA60 /* V */ = {
isa = PBXGroup;
children = (
04FC97072EB31B14007BD342 /* KBHUD.h */,
04FC97082EB31B14007BD342 /* KBHUD.m */,
049FB20C2EC1CD2800FAB05D /* KBAlert.h */,
049FB20D2EC1CD2800FAB05D /* KBAlert.m */,
);
path = V;
sourceTree = "<group>";
@@ -826,6 +836,8 @@
048908EB2EBF849300FABA60 /* KBSkinTagsContainerCell.m */,
048908ED2EBF861800FABA60 /* KBSkinSectionTitleCell.h */,
048908EE2EBF861800FABA60 /* KBSkinSectionTitleCell.m */,
049FB2092EC1C13800FAB05D /* KBSkinBottomActionView.h */,
049FB20A2EC1C13800FAB05D /* KBSkinBottomActionView.m */,
);
path = V;
sourceTree = "<group>";
@@ -967,8 +979,6 @@
04FC970B2EB334F8007BD342 /* UIImageView+KBWebImage.m */,
04FC970C2EB334F8007BD342 /* KBWebImageManager.h */,
04FC970D2EB334F8007BD342 /* KBWebImageManager.m */,
04FC97072EB31B14007BD342 /* KBHUD.h */,
04FC97082EB31B14007BD342 /* KBHUD.m */,
04A9FE142EB873C80020DB6D /* UIViewController+Extension.h */,
04A9FE152EB873C80020DB6D /* UIViewController+Extension.m */,
047C655A2EBCD08E0035E841 /* UIView+KBShadow.h */,
@@ -1346,6 +1356,7 @@
0477BE002EBC6A330055D639 /* HomeRankVC.m in Sources */,
047C650D2EBC8A840035E841 /* KBPanModalView.m in Sources */,
043FBCD22EAF97630036AFE1 /* KBPermissionViewController.m in Sources */,
049FB20E2EC1CD2800FAB05D /* KBAlert.m in Sources */,
04A9FE162EB873C80020DB6D /* UIViewController+Extension.m in Sources */,
04C6EABE2EAF86530089C901 /* AppDelegate.m in Sources */,
04FC95F12EB339A7007BD342 /* LoginViewController.m in Sources */,
@@ -1365,7 +1376,7 @@
047C65102EBCA8DD0035E841 /* HomeRankContentVC.m in Sources */,
047C655C2EBCD0F80035E841 /* UIView+KBShadow.m in Sources */,
048908C32EBE32B800FABA60 /* KBSearchVC.m in Sources */,
7276DDA82EC1B28300804C36 /* KBWebViewViewController.m in Sources */,
049FB20B2EC1C13800FAB05D /* KBSkinBottomActionView.m in Sources */,
047C655E2EBCD5B20035E841 /* UIImage+KBColor.m in Sources */,
04FC95DD2EB202A3007BD342 /* KBGuideVC.m in Sources */,
04FC95E52EB220B5007BD342 /* UIColor+Extension.m in Sources */,

View File

@@ -3,7 +3,6 @@
// keyBoard
//
// 统一封装基于 LYEmptyView 的空态视图挂载方法,适用于 UITableView/UICollectionView。
// 注意:仅在对应页面已通过 CocoaPods 集成 LYEmptyView 时生效。
//
#import <UIKit/UIKit.h>

View File

@@ -34,13 +34,25 @@ static inline __kindof UIWindow *KBActiveWindow(void) {
+ (UIViewController *)kb_topMostViewController {
UIWindow *window = KBActiveWindow();
UIViewController *root = window.rootViewController;
if (!root) return nil;
UIViewController *top = root;
while (top.presentedViewController) {
top = top.presentedViewController;
UIViewController *vc = window.rootViewController;
if (!vc) return nil;
//
while (1) {
if ([vc isKindOfClass:UINavigationController.class]) {
vc = ((UINavigationController *)vc).visibleViewController ?: vc;
continue;
}
if ([vc isKindOfClass:UITabBarController.class]) {
vc = ((UITabBarController *)vc).selectedViewController ?: vc;
continue;
}
if (vc.presentedViewController) {
vc = vc.presentedViewController;
continue;
}
break;
}
return top;
return vc;
}
@end

View File

@@ -0,0 +1,83 @@
//
// KBAlert.h
// keyBoard
//
// 系统 UIAlertController 的轻量封装:更易用、更健壮。
// 特性:
// - 任意线程调用;内部切回主线程
// - 自动找到可展示的 VC使用顶层 presented VC
// - Alert 队列:避免“正在展示时再次 present 失败”的崩溃/警告
// - iPad ActionSheet 自动设置 popover 锚点(无需关心 sourceView
// - 常见场景一行调用:提示/确认/输入框/操作表
// - 可选指定展示的 VC如在 Extension 内)
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef void (^KBAlertBoolHandler)(BOOL ok);
typedef void (^KBAlertIndexHandler)(NSInteger index);
typedef void (^KBAlertTextHandler)(NSString * _Nullable text, BOOL ok);
@interface KBAlert : NSObject
/// 指定一个缺省的“用于 present 的 VC”。
/// - 适用App Extension 或自管理容器场景App 内一般无需设置。
/// - 注意:弱引用;随时可被释放。
+ (void)setDefaultPresenter:(nullable __kindof UIViewController *)presenter;
#pragma mark - 简单提示(单按钮)
/// 标准 OK 提示按钮文案默认为“好”或“OK”。
+ (void)showTitle:(nullable NSString *)title message:(nullable NSString *)message;
/// 自定义按钮文案与回调。
+ (void)showTitle:(nullable NSString *)title
message:(nullable NSString *)message
button:(nullable NSString *)button
completion:(dispatch_block_t)completion;
#pragma mark - 确认对话框(双按钮)
/// 默认“取消/确定”按钮,回调 ok=YES 表示点击了确定。
+ (void)confirmTitle:(nullable NSString *)title
message:(nullable NSString *)message
completion:(KBAlertBoolHandler)completion;
/// 自定义按钮文案。
+ (void)confirmTitle:(nullable NSString *)title
message:(nullable NSString *)message
ok:(nullable NSString *)okTitle
cancel:(nullable NSString *)cancelTitle
completion:(KBAlertBoolHandler)completion;
/// 自定义“确定/取消”按钮颜色(为 nil 时使用系统默认)
+ (void)confirmTitle:(nullable NSString *)title
message:(nullable NSString *)message
ok:(nullable NSString *)okTitle
cancel:(nullable NSString *)cancelTitle
okColor:(nullable UIColor *)okColor
cancelColor:(nullable UIColor *)cancelColor
completion:(KBAlertBoolHandler)completion;
#pragma mark - 输入框(单行)
/// 带单个输入框ok=YES 时返回输入内容。
+ (void)promptTitle:(nullable NSString *)title
message:(nullable NSString *)message
placeholder:(nullable NSString *)placeholder
ok:(nullable NSString *)okTitle
cancel:(nullable NSString *)cancelTitle
keyboardType:(UIKeyboardType)type
configuration:(void (^ _Nullable)(UITextField *tf))config
completion:(KBAlertTextHandler)completion;
#pragma mark - 操作表ActionSheet
/// 操作表index 为点击项序号,按 actions 顺序从 0 开始;取消返回 -1。
+ (void)actionSheetTitle:(nullable NSString *)title
message:(nullable NSString *)message
actions:(NSArray<NSString *> *)actions
cancel:(nullable NSString *)cancelTitle
destructiveIndex:(NSInteger)destructiveIndex // 传入 -1 表示无
fromView:(nullable UIView *)anchorView // iPad 可指定锚点;传 nil 自动居中
completion:(KBAlertIndexHandler)completion;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,243 @@
//
// KBAlert.m
// keyBoard
//
#import "KBAlert.h"
#import <UIKit/UIKit.h>
#import "UIViewController+Extension.h"
@implementation KBAlert
// present
static NSMutableArray<dispatch_block_t> *sQueue;
static BOOL sPresenting = NO;
static __weak UIViewController *sDefaultPresenter = nil; // presenter
#pragma mark - Helpers
+ (void)initialize {
if (self == KBAlert.class) {
sQueue = [NSMutableArray array];
}
}
+ (void)onMain:(dispatch_block_t)blk {
if (NSThread.isMainThread) { blk(); } else { dispatch_async(dispatch_get_main_queue(), blk); }
}
+ (UIViewController *)presentingVC {
// 使 presenter
UIViewController *vc = sDefaultPresenter;
if (!vc) {
vc = [UIViewController kb_topMostViewController];
}
return vc;
}
+ (void)enqueuePresent:(dispatch_block_t)presenter {
[self onMain:^{
[sQueue addObject:[presenter copy]];
if (!sPresenting) { [self _dequeueAndPresentNext]; }
}];
}
+ (void)_dequeueAndPresentNext {
if (sPresenting || sQueue.count == 0) { return; }
sPresenting = YES;
dispatch_block_t blk = sQueue.firstObject;
[sQueue removeObjectAtIndex:0];
blk();
}
// action
+ (void)_markFinishedAndContinue {
// runloop dismiss
dispatch_async(dispatch_get_main_queue(), ^{
sPresenting = NO;
[self _dequeueAndPresentNext];
});
}
// iPad ActionSheet popover
+ (void)configurePopoverIfNeeded:(UIAlertController *)ac anchorView:(UIView *)anchorView inVC:(UIViewController *)vc {
if (ac.preferredStyle != UIAlertControllerStyleActionSheet) { return; }
UIPopoverPresentationController *pop = ac.popoverPresentationController;
if (!pop) { return; }
if (anchorView) {
pop.sourceView = anchorView;
pop.sourceRect = anchorView.bounds;
} else if (vc.view) {
pop.sourceView = vc.view;
CGSize sz = vc.view.bounds.size;
CGRect r = CGRectMake(MAX(0, sz.width * 0.5 - 1), MAX(0, sz.height * 0.5 - 1), 2, 2);
pop.sourceRect = r; //
pop.permittedArrowDirections = 0;
}
}
#pragma mark - Public API
+ (void)setDefaultPresenter:(UIViewController *)presenter { sDefaultPresenter = presenter; }
+ (void)showTitle:(NSString *)title message:(NSString *)message {
[self showTitle:title message:message button:nil completion:nil];
}
+ (void)showTitle:(NSString *)title
message:(NSString *)message
button:(NSString *)button
completion:(dispatch_block_t)completion {
[self enqueuePresent:^{
UIViewController *vc = [self presentingVC];
if (!vc) { // presenter
[self _markFinishedAndContinue];
return;
}
NSString *ok = button ?: (NSLocalizedString(@"OK", nil).length ? NSLocalizedString(@"OK", nil) : @"好");
UIAlertController *ac = [UIAlertController alertControllerWithTitle:(title ?: @"")
message:(message ?: @"")
preferredStyle:UIAlertControllerStyleAlert];
__weak UIAlertController *wac = ac;
UIAlertAction *okAct = [UIAlertAction actionWithTitle:ok style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction * _Nonnull action) {
if (completion) { completion(); }
// alert
[wac dismissViewControllerAnimated:YES completion:nil];
[KBAlert _markFinishedAndContinue];
}];
[ac addAction:okAct];
[vc presentViewController:ac animated:YES completion:nil];
}];
}
+ (void)confirmTitle:(NSString *)title message:(NSString *)message completion:(KBAlertBoolHandler)completion {
[self confirmTitle:title message:message ok:nil cancel:nil completion:completion];
}
+ (void)confirmTitle:(NSString *)title
message:(NSString *)message
ok:(NSString *)okTitle
cancel:(NSString *)cancelTitle
completion:(KBAlertBoolHandler)completion {
[self confirmTitle:title message:message ok:okTitle cancel:cancelTitle okColor:nil cancelColor:nil completion:completion];
}
// KVC action API
static inline void KBSetActionTitleColor(UIAlertAction *action, UIColor *color) {
if (!color || !action) return;
@try { [action setValue:color forKey:@"titleTextColor"]; } @catch (__unused NSException *e) {}
}
+ (void)confirmTitle:(NSString *)title
message:(NSString *)message
ok:(NSString *)okTitle
cancel:(NSString *)cancelTitle
okColor:(UIColor *)okColor
cancelColor:(UIColor *)cancelColor
completion:(KBAlertBoolHandler)completion {
[self enqueuePresent:^{
UIViewController *vc = [self presentingVC];
if (!vc) { [self _markFinishedAndContinue]; return; }
NSString *ok = okTitle ?: @"确定";
NSString *cancel = cancelTitle ?: @"取消";
UIAlertController *ac = [UIAlertController alertControllerWithTitle:(title ?: @"")
message:(message ?: @"")
preferredStyle:UIAlertControllerStyleAlert];
__weak UIAlertController *wac = ac;
UIAlertAction *cancelAct = [UIAlertAction actionWithTitle:cancel style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction * _Nonnull a) {
if (completion) { completion(NO); }
[wac dismissViewControllerAnimated:YES completion:nil];
[KBAlert _markFinishedAndContinue];
}];
UIAlertAction *okAct = [UIAlertAction actionWithTitle:ok style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction * _Nonnull a) {
if (completion) { completion(YES); }
[wac dismissViewControllerAnimated:YES completion:nil];
[KBAlert _markFinishedAndContinue];
}];
//
KBSetActionTitleColor(okAct, okColor);
KBSetActionTitleColor(cancelAct, cancelColor);
[ac addAction:cancelAct];
[ac addAction:okAct];
[vc presentViewController:ac animated:YES completion:nil];
}];
}
+ (void)promptTitle:(NSString *)title
message:(NSString *)message
placeholder:(NSString *)placeholder
ok:(NSString *)okTitle
cancel:(NSString *)cancelTitle
keyboardType:(UIKeyboardType)type
configuration:(void (^)(UITextField * _Nonnull))config
completion:(KBAlertTextHandler)completion {
[self enqueuePresent:^{
UIViewController *vc = [self presentingVC];
if (!vc) { [self _markFinishedAndContinue]; return; }
NSString *ok = okTitle ?: @"确定";
NSString *cancel = cancelTitle ?: @"取消";
UIAlertController *ac = [UIAlertController alertControllerWithTitle:(title ?: @"")
message:(message ?: @"")
preferredStyle:UIAlertControllerStyleAlert];
__weak UIAlertController *wac = ac;
[ac addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull tf) {
tf.placeholder = placeholder;
tf.keyboardType = type;
if (config) { config(tf); }
}];
UIAlertAction *cancelAct = [UIAlertAction actionWithTitle:cancel style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction * _Nonnull a) {
if (completion) { completion(nil, NO); }
[wac dismissViewControllerAnimated:YES completion:nil];
[KBAlert _markFinishedAndContinue];
}];
UIAlertAction *okAct = [UIAlertAction actionWithTitle:ok style:UIAlertActionStyleDefault handler:^(__unused UIAlertAction * _Nonnull a) {
NSString *text = wac.textFields.firstObject.text;
if (completion) { completion(text, YES); }
[wac dismissViewControllerAnimated:YES completion:nil];
[KBAlert _markFinishedAndContinue];
}];
[ac addAction:cancelAct];
[ac addAction:okAct];
[vc presentViewController:ac animated:YES completion:nil];
}];
}
+ (void)actionSheetTitle:(NSString *)title
message:(NSString *)message
actions:(NSArray<NSString *> *)actions
cancel:(NSString *)cancelTitle
destructiveIndex:(NSInteger)destructiveIndex
fromView:(UIView *)anchorView
completion:(KBAlertIndexHandler)completion {
[self enqueuePresent:^{
UIViewController *vc = [self presentingVC];
if (!vc) { [self _markFinishedAndContinue]; return; }
UIAlertController *ac = [UIAlertController alertControllerWithTitle:(title ?: @"")
message:(message ?: @"")
preferredStyle:UIAlertControllerStyleActionSheet];
__weak UIAlertController *wac = ac;
[actions enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
UIAlertActionStyle style = (destructiveIndex >= 0 && (NSInteger)idx == destructiveIndex) ? UIAlertActionStyleDestructive : UIAlertActionStyleDefault;
UIAlertAction *act = [UIAlertAction actionWithTitle:obj ?: @"" style:style handler:^(__unused UIAlertAction * _Nonnull a) {
if (completion) { completion((NSInteger)idx); }
[wac dismissViewControllerAnimated:YES completion:nil];
[KBAlert _markFinishedAndContinue];
}];
[ac addAction:act];
}];
if (cancelTitle.length) {
UIAlertAction *cancel = [UIAlertAction actionWithTitle:cancelTitle style:UIAlertActionStyleCancel handler:^(__unused UIAlertAction * _Nonnull a) {
if (completion) { completion(-1); }
[wac dismissViewControllerAnimated:YES completion:nil];
[KBAlert _markFinishedAndContinue];
}];
[ac addAction:cancel];
}
[self configurePopoverIfNeeded:ac anchorView:anchorView inVC:vc];
[vc presentViewController:ac animated:YES completion:nil];
}];
}
@end

View File

@@ -81,9 +81,9 @@
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView deselectRowAtIndexPath:indexPath animated:YES];
// KBSearchVC *vc = [[KBSearchVC alloc] init];
KBSearchVC *vc = [[KBSearchVC alloc] init];
// [self.navigationController pushViewController:vc animated:true];
// UINavigationController *nav = KB_CURRENT_NAV;
UINavigationController *nav = KB_CURRENT_NAV;
// [nav pushViewController:vc animated:true];
NSLog(@"===");

View File

@@ -75,7 +75,7 @@
desc:d[@"desc"]
people:d[@"people"]
added:added];
__weak typeof(self) weakSelf = self;
KBWeakSelf
cell.onTapAction = ^{
// /
NSMutableArray *m = [weakSelf.dataSource mutableCopy];

View File

@@ -132,7 +132,7 @@
- (void)onContinue {
LoginViewController *login = [LoginViewController new];
__weak typeof(self) weakSelf = self;
KBWeakSelf
login.onLoginSuccess = ^(NSDictionary * _Nonnull userInfo) {
__strong typeof(weakSelf) self = weakSelf; if (!self) return;
[self dismissViewControllerAnimated:YES completion:^{

View File

@@ -114,7 +114,7 @@
}
- (void)handleAppleIDButtonPress API_AVAILABLE(ios(13.0)) {
__weak typeof(self) weakSelf = self;
KBWeakSelf
[[AppleSignInManager shared] signInFromViewController:self completion:^(ASAuthorizationAppleIDCredential * _Nullable credential, NSError * _Nullable error) {
__strong typeof(weakSelf) selfStrong = weakSelf;
if (error) {

View File

@@ -0,0 +1,37 @@
//
// KBSkinBottomActionView.h
// keyBoard
//
// 底部操作条(圆角胶囊样式),支持点击。用于皮肤详情页下载/购买。
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/// 底部操作条45 高,左右圆角)。
/// 结构: [ Title [coinIcon] [price] ] 居中对齐
@interface KBSkinBottomActionView : UIControl
/// 标题,例如:@"Download"
@property (nonatomic, copy) NSString *titleText;
/// 价格/金币数,例如:@"20"
@property (nonatomic, copy) NSString *priceText;
/// 图标(可选),例如金币图
@property (nonatomic, strong, nullable) UIImage *iconImage;
/// 点击回调(也可直接 addTarget 使用)
@property (nonatomic, copy, nullable) void (^tapHandler)(void);
/// 配置便捷方法
- (void)configWithTitle:(nullable NSString *)title price:(nullable NSString *)price icon:(nullable UIImage *)icon;
/// 建议固定高度
+ (CGFloat)preferredHeight;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,153 @@
//
// KBSkinBottomActionView.m
// keyBoard
//
//
#import "KBSkinBottomActionView.h"
@interface KBSkinBottomActionView ()
@property (nonatomic, strong) UIView *contentView; // 使
@property (nonatomic, strong) UILabel *titleLabel; //
@property (nonatomic, strong) UIImageView *coinImageView; //
@property (nonatomic, strong) UILabel *priceLabel; //
@end
@implementation KBSkinBottomActionView
+ (CGFloat)preferredHeight { return 45.0; }
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
self.backgroundColor = [UIColor colorWithHex:0x02BEAC];
self.layer.masksToBounds = YES; //
//
[self addTarget:self action:@selector(onTouchDown) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:@selector(onTouchUp) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchUpOutside | UIControlEventTouchCancel];
// /
[self addSubview:self.contentView];
[self.contentView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self);
make.centerY.equalTo(self);
//
make.left.greaterThanOrEqualTo(self).offset(16);
make.right.lessThanOrEqualTo(self).offset(-16);
}];
// Title - Icon - Price
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.coinImageView];
[self.contentView addSubview:self.priceLabel];
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView);
make.centerY.equalTo(self.contentView);
}];
[self.coinImageView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.titleLabel.mas_right).offset(8);
make.centerY.equalTo(self.contentView);
make.width.height.mas_equalTo(18);
}];
[self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.coinImageView.mas_right).offset(6);
make.right.equalTo(self.contentView);
make.centerY.equalTo(self.contentView);
}];
//
self.titleText = @"Download";
self.priceText = @"20";
UIImage *img = [UIImage systemImageNamed:@"circle.fill"];
self.iconImage = img; //
self.coinImageView.tintColor = [UIColor colorWithRed:1.0 green:0.85 blue:0.2 alpha:1.0];
//
[self addTarget:self action:@selector(handleTap) forControlEvents:UIControlEventTouchUpInside];
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
//
self.layer.cornerRadius = CGRectGetHeight(self.bounds) * 0.5;
}
#pragma mark - Public
- (void)configWithTitle:(nullable NSString *)title price:(nullable NSString *)price icon:(nullable UIImage *)icon {
if (title.length) self.titleText = title;
if (price.length) self.priceText = price;
if (icon) self.iconImage = icon;
}
#pragma mark - Actions
- (void)onTouchDown { self.alpha = 0.85; }
- (void)onTouchUp { self.alpha = 1.0; }
- (void)handleTap {
if (self.tapHandler) { self.tapHandler(); }
}
#pragma mark - Setters
- (void)setTitleText:(NSString *)titleText {
_titleText = [titleText copy];
self.titleLabel.text = _titleText;
}
- (void)setPriceText:(NSString *)priceText {
_priceText = [priceText copy];
self.priceLabel.text = _priceText;
}
- (void)setIconImage:(UIImage *)iconImage {
_iconImage = iconImage;
self.coinImageView.image = _iconImage;
}
#pragma mark - Lazy
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [[UILabel alloc] init];
_titleLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
_titleLabel.textColor = [UIColor whiteColor];
_titleLabel.textAlignment = NSTextAlignmentCenter;
}
return _titleLabel;
}
- (UIView *)contentView {
if (!_contentView) {
_contentView = [[UIView alloc] init];
_contentView.backgroundColor = [UIColor clearColor];
//
[_contentView setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[_contentView setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
}
return _contentView;
}
- (UIImageView *)coinImageView {
if (!_coinImageView) {
_coinImageView = [[UIImageView alloc] init];
_coinImageView.contentMode = UIViewContentModeScaleAspectFit;
_coinImageView.clipsToBounds = YES;
}
return _coinImageView;
}
- (UILabel *)priceLabel {
if (!_priceLabel) {
_priceLabel = [[UILabel alloc] init];
_priceLabel.font = [UIFont systemFontOfSize:17 weight:UIFontWeightSemibold];
_priceLabel.textColor = [UIColor whiteColor];
_priceLabel.textAlignment = NSTextAlignmentCenter;
}
return _priceLabel;
}
@end

View File

@@ -8,8 +8,9 @@
#import "UICollectionViewLeftAlignedLayout.h"
#import "KBSkinDetailHeaderCell.h"
#import "KBSkinTagsContainerCell.h"
#import "KBSkinCardCell.h" // cell 2
#import "KBSkinCardCell.h"
#import "KBSkinSectionTitleCell.h"
#import "KBSkinBottomActionView.h"
static NSString * const kHeaderCellId = @"kHeaderCellId";
static NSString * const kTagsContainerCellId = @"kTagsContainerCellId";
@@ -26,6 +27,7 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
@interface KBSkinDetailVC () <UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, strong) UICollectionView *collectionView; //
@property (nonatomic, strong) KBSkinBottomActionView *bottomBar; //
@property (nonatomic, copy) NSArray<NSString *> *tags; //
@property (nonatomic, copy) NSArray<NSDictionary *> *gridData; //
@end
@@ -44,9 +46,26 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
@{ @"title": @"Dopamine" }, @{ @"title": @"Dopamine" },
];
// 1.
[self.view addSubview:self.collectionView];
// 2. 15 45
[self.view addSubview:self.bottomBar];
[self.bottomBar mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view).offset(15);
make.right.equalTo(self.view).offset(-15);
make.height.mas_equalTo([KBSkinBottomActionView preferredHeight]);
if (@available(iOS 11.0, *)) {
make.bottom.equalTo(self.view.mas_safeAreaLayoutGuideBottom);
} else {
make.bottom.equalTo(self.view);
}
}];
// 10
[self.collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(self.view);
make.top.left.right.equalTo(self.view);
make.bottom.equalTo(self.bottomBar.mas_top).offset(-10);
}];
}
@@ -156,4 +175,27 @@ typedef NS_ENUM(NSInteger, KBSkinDetailSection) {
return _collectionView;
}
#pragma mark - Lazy (Bottom Bar)
- (KBSkinBottomActionView *)bottomBar {
if (!_bottomBar) {
_bottomBar = [[KBSkinBottomActionView alloc] init];
// /
[_bottomBar configWithTitle:@"Download" price:@"20" icon:nil];
KBWeakSelf
_bottomBar.tapHandler = ^{
// /
// [KBHUD showText:@"点击了下载"];
// TODO: /
[weakSelf handleDownloadAction];
};
}
return _bottomBar;
}
#pragma mark - Actions
- (void)handleDownloadAction {
// /
}
@end

View File

@@ -55,7 +55,7 @@ static NSString * const kMySkinCellId = @"kMySkinCellId";
}];
// LYEmptyView +
__weak typeof(self) weakSelf = self;
KBWeakSelf
[self.collectionView kb_makeDefaultEmptyViewWithImage:nil
title:@"暂无皮肤"
detail:@"下拉刷新试试"

View File

@@ -29,10 +29,10 @@
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
MySkinVC *vc = [[MySkinVC alloc] init];
// KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
// MySkinVC *vc = [[MySkinVC alloc] init];
KBSkinDetailVC *vc = [[KBSkinDetailVC alloc] init];
[self.navigationController pushViewController:vc animated:true];
// [self.navigationController pushViewController:vc animated:true];
}
/*

View File

@@ -20,10 +20,14 @@ NS_ASSUME_NONNULL_BEGIN
/// 配置标题与是否显示垃圾桶
- (void)configWithTitle:(NSString *)title showTrash:(BOOL)showTrash;
/// 清除按钮点击回调
/// 清除按钮点击回调(用户在弹窗中点击“确定”后触发)
@property (nonatomic, copy) void(^onTapTrash)(void);
/// “确定”按钮颜色(默认黑色)
@property (nonatomic, strong, nullable) UIColor *confirmColor;
/// “取消”按钮颜色(默认黑色)
@property (nonatomic, strong, nullable) UIColor *cancelColor;
@end
NS_ASSUME_NONNULL_END

View File

@@ -4,6 +4,7 @@
//
#import "KBSearchSectionHeader.h"
#import "KBAlert.h"
@interface KBSearchSectionHeader ()
@property (nonatomic, strong, readwrite) UILabel *titleLabel;
@@ -15,6 +16,8 @@
- (instancetype)initWithFrame:(CGRect)frame {
if (self = [super initWithFrame:frame]) {
[self setupUI];
_confirmColor = [UIColor redColor];
_cancelColor = [UIColor blackColor];
}
return self;
}
@@ -39,7 +42,19 @@
}
- (void)tapTrash {
if (self.onTapTrash) { self.onTapTrash(); }
//
__weak typeof(self) weakSelf = self;
[KBAlert confirmTitle:@"清空历史"
message:@"是否删除所有历史记录?"
ok:@"确定"
cancel:@"取消"
okColor:weakSelf.confirmColor
cancelColor:weakSelf.cancelColor
completion:^(BOOL ok) {
if (ok && weakSelf.onTapTrash) {
weakSelf.onTapTrash();
}
}];
}
- (void)configWithTitle:(NSString *)title showTrash:(BOOL)showTrash {
@@ -69,4 +84,3 @@
}
@end

View File

@@ -152,7 +152,7 @@ static NSString * const kResultCellId = @"KBSkinCardCell";
if (!_searchBarView) {
_searchBarView = [[KBSearchBarView alloc] init];
_searchBarView.placeholder = @"Themes";
__weak typeof(self) weakSelf = self;
KBWeakSelf
_searchBarView.onSearch = ^(NSString * _Nonnull keyword) {
[weakSelf performSearch:keyword];
};

View File

@@ -285,7 +285,7 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
if (indexPath.section == KBSearchSectionHistory) {
// sizeForHeader 0
[header configWithTitle:@"Historical Search" showTrash:self.historyWords.count > 0];
__weak typeof(self) weakSelf = self;
KBWeakSelf
header.onTapTrash = ^{ [weakSelf clearHistory]; };
} else {
[header configWithTitle:@"Recommended Skin" showTrash:NO];
@@ -363,7 +363,7 @@ typedef NS_ENUM(NSInteger, KBSearchSection) {
if (!_searchBarView) {
_searchBarView = [[KBSearchBarView alloc] init];
_searchBarView.placeholder = @"Themes";
__weak typeof(self) weakSelf = self;
KBWeakSelf
_searchBarView.onSearch = ^(NSString * _Nonnull keyword) {
// +
[weakSelf performSearch:keyword];

View File

@@ -27,7 +27,7 @@
}];
// 2
__weak typeof(self) weakSelf = self;
KBWeakSelf
if (self.isNeedHeader) {
self.collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

View File

@@ -21,6 +21,7 @@
/// 系统
#import <UIKit/UIKit.h>
#import "KBHUD.h"
#import "KBAlert.h"
#import "KBNetworkManager.h"