首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 綜合知識

iOS 自定義彈出框實作:類似 UIAlertView 的完整元件封裝 (Objective-C)

     編輯:PHP 綜合知識

專案背景與設計目標

iOS 7+ 棄用 UIAlertView/UIActionSheet,改用 UIAlertController。但實際專案常需:

  • 高度自定義佈局 (圖片、多行文字、自定義按鈕樣式)
  • 非模態顯示、特殊動畫、背景虛化效果
  • 支援 iOS 6 相容 (舊專案維護)
  • 區塊回調語法優於委派模式

本文實作一個 純代碼、無圖片資源、高度可配置 的 XLAlertView 元件。

介面定義 (XLAlertView.h)

#import 

typedef 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

核心實作 (XLAlertView.m) - 關鍵邏輯解析

#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 封裝

與 UIAlertController 對比

特性XLAlertView (自定義)UIAlertController (系統)
佈局自由度✅ 完全可控❌ 固定樣式
動畫自定義✅ 彈性、縮放、轉場❌ 系統預設
iOS 版本支援✅ iOS 6+✅ iOS 8+
維護成本⚠️ 自行維護✅ 系統維護
系統一致性⚠️ 需手動跟隨設計規範✅ 自動適配
安全區域適配⚠️ 需手動處理✅ 自動處理

總結

XLAlertView 展示了 iOS 自定義彈出框的完整實作要素:

  1. 視圖層級:全螢幕容器 + 居中彈出框 + 半透明背景
  2. 自適應佈局:sizeToFit + 動態高度計算 + 可選元素條件渲染
  3. 圓角遮罩:CAShapeLayer + UIBezierPath 實現精確圓角
  4. 彈性動畫:usingSpringWithDamping 實現 iOS 原生質感
  5. 區塊回調:typedef void(^Block)(NSInteger) 簡潔於委派模式
  6. 記憶體安全:copy 修飾 Block、weakSelf 避免循環引用 (實際專案需補充)

現代新專案建議優先使用 UIAlertController + 自定義 UIViewController (透過 preferredContentSize、modalPresentationStyle);維護舊專案或需極致自定義時,本元件提供可靠參考實作。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved