244 lines
10 KiB
Objective-C
244 lines
10 KiB
Objective-C
//
|
||
// 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
|