iOS 7+ 棄用 UIAlertView/UIActionSheet,改用 UIAlertController。但實際專案常需:
本文實作一個 純代碼、無圖片資源、高度可配置 的 XLAlertView 元件。
#importtypedef void(^XLAlertResultBlock)(NSInteger buttonIndex); @interface XLAlertView : UIView /** 回調區塊:點擊按鈕時觸發,參數為按鈕索引 (0=取消, 1=確定) */ @property (nonatomic, copy) XLAlertResultBlock resultBlock; /** 初始化彈出框 @param title 標題 (可為 nil) @param message 內容訊息 (可為 nil) @param sureTitle 確定按鈕標題 (nil 則不顯示) @param cancelTitle 取消按鈕標題 (nil 則不顯示) @return 初始化實例 */ - (instancetype)initWithTitle:(NSString *)title message:(NSString *)message sureBtn:(NSString *)sureTitle cancleBtn:(NSString *)cancleTitle; /** 顯示彈出框 (自動添加到 keyWindow) */ - (void)show; @end
#import "XLAlertView.h"
#define kAlertWidth 280.0f
#define kSpacing 10.0f
@interface XLAlertView ()
// 子視圖
@property (nonatomic, strong) UIView *alertView; // 彈出框容器
@property (nonatomic, strong) UILabel *titleLabel; // 標題
@property (nonatomic, strong) UILabel *messageLabel; // 內容
@property (nonatomic, strong) UIButton *sureButton; // 確定按鈕
@property (nonatomic, strong) UIButton *cancelButton; // 取消按鈕
@property (nonatomic, strong) UIView *horizontalLine; // 水平分隔線
@property (nonatomic, strong) UIView *verticalLine; // 垂直分隔線 (雙按鈕時)
@end
@implementation XLAlertView
- (instancetype)initWithTitle:(NSString *)title
message:(NSString *)message
sureBtn:(NSString *)sureTitle
cancleBtn:(NSString *)cancleTitle {
if (self = [super init]) {
// 1. 根視圖設定:全螢幕、半透明黑背景
self.frame = [UIScreen mainScreen].bounds;
self.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.6];
// 2. 彈出框容器:白背景、圓角
_alertView = [[UIView alloc] init];
_alertView.backgroundColor = [UIColor whiteColor];
_alertView.layer.cornerRadius = 5.0;
_alertView.layer.masksToBounds = YES;
// 初始 frame 暫定,後續根據內容動態計算高度
_alertView.frame = CGRectMake(0, 0, kAlertWidth, 100);
_alertView.center = self.center;
CGFloat currentY = kSpacing * 2; // 內容起始 Y
// 3. 標題標籤 (可選)
if (title.length > 0) {
_titleLabel = [self adaptiveLabelWithText:title
isTitle:YES
maxWidth:kAlertWidth - 4*kSpacing];
_titleLabel.textAlignment = NSTextAlignmentCenter;
_titleLabel.frame = CGRectMake(
(kAlertWidth - _titleLabel.bounds.size.width) / 2,
currentY,
_titleLabel.bounds.size.width,
_titleLabel.bounds.size.height
);
[_alertView addSubview:_titleLabel];
currentY = CGRectGetMaxY(_titleLabel.frame) + kSpacing;
}
// 4. 內容標籤 (可選)
if (message.length > 0) {
_messageLabel = [self adaptiveLabelWithText:message
isTitle:NO
maxWidth:kAlertWidth - 2*kSpacing];
_messageLabel.textAlignment = NSTextAlignmentCenter;
_messageLabel.frame = CGRectMake(
(kAlertWidth - _messageLabel.bounds.size.width) / 2,
currentY,
_messageLabel.bounds.size.width,
_messageLabel.bounds.size.height
);
[_alertView addSubview:_messageLabel];
currentY = CGRectGetMaxY(_messageLabel.frame) + kSpacing * 2;
}
// 5. 水平分隔線
_horizontalLine = [[UIView alloc] init];
_horizontalLine.frame = CGRectMake(0, currentY, kAlertWidth, 1);
_horizontalLine.backgroundColor = [UIColor colorWithWhite:0.8 alpha:0.6];
[_alertView addSubview:_horizontalLine];
currentY = CGRectGetMaxY(_horizontalLine.frame);
// 6. 按鈕配置邏輯 (四種組合)
BOOL hasCancel = cancleTitle.length > 0;
BOOL hasSure = sureTitle.length > 0;
CGFloat btnHeight = 40.0f;
if (hasCancel && hasSure) {
// 雙按鈕:各佔半寬,中間有垂直分隔線
_cancelButton = [self buttonWithTitle:cancleTitle tag:0];
_cancelButton.frame = CGRectMake(0, currentY, (kAlertWidth-1)/2, btnHeight);
[_alertView addSubview:_cancelButton];
_verticalLine = [[UIView alloc] init];
_verticalLine.frame = CGRectMake(CGRectGetMaxX(_cancelButton.frame), currentY, 1, btnHeight);
_verticalLine.backgroundColor = [UIColor colorWithWhite:0.8 alpha:0.6];
[_alertView addSubview:_verticalLine];
_sureButton = [self buttonWithTitle:sureTitle tag:1];
_sureButton.frame = CGRectMake(CGRectGetMaxX(_verticalLine.frame), currentY, (kAlertWidth-1)/2+1, btnHeight);
[_alertView addSubview:_sureButton];
// 圓角遮罩:左下/右下
[self applyCornerMask:_cancelButton corners:UIRectCornerBottomLeft];
[self applyCornerMask:_sureButton corners:UIRectCornerBottomRight];
}
else if (hasCancel) {
// 僅取消按鈕:全寬,底部圓角
_cancelButton = [self buttonWithTitle:cancleTitle tag:0];
_cancelButton.frame = CGRectMake(0, currentY, kAlertWidth, btnHeight);
[_alertView addSubview:_cancelButton];
[self applyCornerMask:_cancelButton corners:UIRectCornerBottomLeft | UIRectCornerBottomRight];
}
else if (hasSure) {
// 僅確定按鈕:全寬,底部圓角
_sureButton = [self buttonWithTitle:sureTitle tag:1];
_sureButton.frame = CGRectMake(0, currentY, kAlertWidth, btnHeight);
[_alertView addSubview:_sureButton];
[self applyCornerMask:_sureButton corners:UIRectCornerBottomLeft | UIRectCornerBottomRight];
}
// 7. 更新彈出框最終高度與位置
CGFloat finalHeight = hasCancel ? CGRectGetMaxY(_cancelButton.frame) : CGRectGetMaxY(_sureButton.frame);
_alertView.frame = CGRectMake(0, 0, kAlertWidth, finalHeight);
_alertView.center = self.center;
[self addSubview:_alertView];
}
return self;
}
/** 建立自適應高度標籤 */
- (UILabel *)adaptiveLabelWithText:(NSString *)text isTitle:(BOOL)isTitle maxWidth:(CGFloat)maxWidth {
UILabel *label = [[UILabel alloc] init];
label.numberOfLines = 0;
label.text = text;
label.font = isTitle ? [UIFont boldSystemFontOfSize:16] : [UIFont systemFontOfSize:14];
// 使用 NSAttributedString 設定行距
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc] initWithString:text];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineBreakMode = NSLineBreakByCharWrapping;
style.lineSpacing = 3.0;
[attrStr addAttribute:NSParagraphStyleAttributeName value:style range:NSMakeRange(0, text.length)];
label.attributedText = attrStr;
// 計算自適應尺寸
[label sizeToFit];
// 限制最大寬度
if (label.bounds.size.width > maxWidth) {
label.frame = CGRectMake(0, 0, maxWidth, label.bounds.size.height);
[label sizeToFit]; // 重新計算高度
}
return label;
}
/** 建立按鈕 */
- (UIButton *)buttonWithTitle:(NSString *)title tag:(NSInteger)tag {
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
[btn setTitle:title forState:UIControlStateNormal];
[btn setTitleColor:[UIColor colorWithRed:0.0/255.0 green:122.0/255.0 blue:255.0/255.0 alpha:1.0] forState:UIControlStateNormal];
btn.titleLabel.font = [UIFont systemFontOfSize:16];
btn.tag = tag;
[btn addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
// 背景色 (正常/高亮)
UIColor *bgNormal = [UIColor colorWithWhite:0.95 alpha:1.0];
UIColor *bgHighlight = [UIColor colorWithWhite:0.9 alpha:1.0];
[btn setBackgroundImage:[self imageWithColor:bgNormal] forState:UIControlStateNormal];
[btn setBackgroundImage:[self imageWithColor:bgHighlight] forState:UIControlStateHighlighted];
return btn;
}
/** 按鈕點擊處理 */
- (void)buttonTapped:(UIButton *)sender {
// 僅確定按鈕 (tag=1) 觸發回調,取消按鈕 (tag=0) 僅關閉
if (sender.tag == 1 && self.resultBlock) {
self.resultBlock(sender.tag);
}
[self dismiss];
}
/** 顯示動畫:彈性縮放 */
- (void)show {
UIWindow *window = [UIApplication sharedApplication].keyWindow;
[window addSubview:self];
_alertView.transform = CGAffineTransformMakeScale(0.9, 0.9);
[UIView animateWithDuration:0.25
delay:0
usingSpringWithDamping:0.8
initialSpringVelocity:1
options:UIViewAnimationOptionCurveEaseOut
animations:^{
_alertView.transform = CGAffineTransformIdentity;
} completion:nil];
}
/** 關閉移除 */
- (void)dismiss {
[UIView animateWithDuration:0.15 animations:^{
_alertView.transform = CGAffineTransformMakeScale(0.9, 0.9);
self.alpha = 0;
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
/** 圓角遮罩 */
- (void)applyCornerMask:(UIView *)view corners:(UIRectCorner)corners {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:view.bounds
byRoundingCorners:corners
cornerRadii:CGSizeMake(5, 5)];
CAShapeLayer *mask = [CAShapeLayer layer];
mask.path = path.CGPath;
view.layer.mask = mask;
}
/** 純色生成圖片 (按鈕背景用) */
- (UIImage *)imageWithColor:(UIColor *)color {
CGRect rect = CGRectMake(0, 0, 1, 1);
UIGraphicsBeginImageContext(rect.size);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(ctx, color.CGColor);
CGContextFillRect(ctx, rect);
UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
@end
// ViewController.m
- (void)showCustomAlert {
XLAlertView *alert = [[XLAlertView alloc]
initWithTitle:@"刪除確認"
message:@"確定要刪除此項目嗎?此操作不可恢復。"
sureBtn:@"確定刪除"
cancleBtn:@"取消"];
alert.resultBlock = ^(NSInteger index) {
if (index == 1) {
NSLog(@"用戶確定刪除");
[self performDelete];
} else {
NSLog(@"用戶取消操作");
}
};
[alert show];
}
// 變體:僅確定按鈕 (提示框)
- (void)showInfoAlert {
XLAlertView *alert = [[XLAlertView alloc]
initWithTitle:@"提示"
message:@"資料已成功儲存"
sureBtn:@"知道了"
cancleBtn:nil]; // 不傳取消按鈕
[alert show];
}
// 變體:僅取消按鈕 (極少用)
- (void)showCancelOnly {
XLAlertView *alert = [[XLAlertView alloc]
initWithTitle:nil
message:@"確定離開編輯頁面?"
sureBtn:nil
cancleBtn:@"繼續編輯"];
[alert show];
}
| 需求 | 實作方向 |
|---|---|
| 輸入框彈出框 | 在 init 中添加 UITextField,resultBlock 返回輸入文字 |
| 多按鈕 (3+) | 按鈕陣列佈局,垂直堆疊或水平滾動 |
| 自定義視圖 | initWithCustomView: 替代 title/message |
| 豐富文字/圖片 | messageLabel 支援 NSAttributedString、附加 UIImageView |
| 暗黑模式適配 | 使用 UIColor { light: ..., dark: ... } 動態色 |
| 無障礙支援 | accessibilityLabel、accessibilityTraits、VoiceOver 測試 |
| Swift 版本 | 結構體配置、@MainActor、async/await 封裝 |
| 特性 | XLAlertView (自定義) | UIAlertController (系統) |
|---|---|---|
| 佈局自由度 | ✅ 完全可控 | ❌ 固定樣式 |
| 動畫自定義 | ✅ 彈性、縮放、轉場 | ❌ 系統預設 |
| iOS 版本支援 | ✅ iOS 6+ | ✅ iOS 8+ |
| 維護成本 | ⚠️ 自行維護 | ✅ 系統維護 |
| 系統一致性 | ⚠️ 需手動跟隨設計規範 | ✅ 自動適配 |
| 安全區域適配 | ⚠️ 需手動處理 | ✅ 自動處理 |
XLAlertView 展示了 iOS 自定義彈出框的完整實作要素:
sizeToFit + 動態高度計算 + 可選元素條件渲染CAShapeLayer + UIBezierPath 實現精確圓角usingSpringWithDamping 實現 iOS 原生質感typedef void(^Block)(NSInteger) 簡潔於委派模式copy 修飾 Block、weakSelf 避免循環引用 (實際專案需補充)現代新專案建議優先使用 UIAlertController + 自定義 UIViewController (透過 preferredContentSize、modalPresentationStyle);維護舊專案或需極致自定義時,本元件提供可靠參考實作。