// // KBAlert.m // keyBoard // #import "KBAlert.h" #import #import "UIViewController+Extension.h" @implementation KBAlert // 简单的串行展示队列,避免重复 present 引发的异常 static NSMutableArray *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 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]; }]; [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 *)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