Files
keyboard/keyBoard/Class/Common/V/KBAlert.m
2025-11-17 20:55:11 +08:00

244 lines
10 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.

//
// 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) : KBLocalized(@"OK"));
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 ?: KBLocalized(@"Confirm");
NSString *cancel = cancelTitle ?: KBLocalized(@"Cancel");
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 ?: KBLocalized(@"Confirm");
NSString *cancel = cancelTitle ?: KBLocalized(@"Cancel");
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