iOS开发基础9-文本提示、系统弹窗与自定义 HUD
iOS 提示框(HUD)全解析:文本提示、系统弹窗与自定义 HUD
提示框(HUD,Heads-Up Display)是为用户提供即时反馈的关键 UI 元素。本文详细介绍文本提示框、系统自带弹窗(UIAlertView、UIActionSheet、UIAlertController)以及自定义 HUD 的实现与底层逻辑。
一、文本提示框
文本提示框通过一个 UILabel 以淡入淡出方式展示简短提示信息,适用于非阻塞式的轻量反馈。
实现思路
- 在视图中添加一个
UILabel作为提示框,初始隐藏(alpha = 0)。 - 需要提示时,通过
UIView动画将其淡入,延迟后淡出。
示例代码
Objective-C
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *shopsView;
@property (weak, nonatomic) IBOutlet UIButton *removeBtn;
@property (weak, nonatomic) IBOutlet UIButton *addBtn;
@property (weak, nonatomic) IBOutlet UILabel *hudLabel;
@property (nonatomic, strong) NSMutableArray *shops;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.hudLabel.alpha = 0.0; // 初始隐藏
}
#pragma mark - 按钮事件
- (IBAction)add {
CGFloat shopWidth = 70;
CGFloat shopHeight = 100;
CGFloat colMargin = (self.shopsView.bounds.size.width - 3 * shopWidth) / 2;
CGFloat rowMargin = colMargin;
NSUInteger index = self.shopsView.subviews.count;
NSInteger row = index / 3;
NSInteger col = index % 3;
CGFloat shopX = col * (shopWidth + colMargin);
CGFloat shopY = row * (shopHeight + rowMargin);
XMGShopView *shopView = [XMGShopView shopView];
shopView.frame = CGRectMake(shopX, shopY, shopWidth, shopHeight);
shopView.shop = self.shops[index];
[self.shopsView addSubview:shopView];
self.removeBtn.enabled = YES;
self.addBtn.enabled = self.shopsView.subviews.count < self.shops.count;
if (!self.addBtn.enabled) {
[self showHUDWithText:@"商品柜已经满了,不要再买买买了..."];
}
}
- (IBAction)remove {
UIView *subView = self.shopsView.subviews.lastObject;
[subView removeFromSuperview];
self.removeBtn.enabled = self.shopsView.subviews.count > 0;
self.addBtn.enabled = YES;
if (!self.removeBtn.enabled) {
[self showHUDWithText:@"商品柜已经空了,继续买买买..."];
}
}
#pragma mark - HUD 显示
- (void)showHUDWithText:(NSString *)text {
self.hudLabel.text = text;
// 开始新动画前移除旧动画,避免连续调用时动画叠加
[self.hudLabel.layer removeAllAnimations];
self.hudLabel.alpha = 0.0;
[UIView animateWithDuration:0.3 animations:^{
self.hudLabel.alpha = 1.0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.3 delay:2.0 options:kNilOptions animations:^{
self.hudLabel.alpha = 0.0;
} completion:nil];
}];
}
#pragma mark - 懒加载
- (NSMutableArray *)shops {
if (!_shops) {
NSString *path = [[NSBundle mainBundle] pathForResource:@"shops" ofType:@"plist"];
NSArray *tempArr = [NSArray arrayWithContentsOfFile:path];
_shops = [NSMutableArray array];
for (NSDictionary *dict in tempArr) {
NJShop *shop = [NJShop shopWithDict:dict];
[_shops addObject:shop];
}
}
return _shops;
}
@end
Swift
class ViewController: UIViewController {
@IBOutlet weak var shopsView: UIView!
@IBOutlet weak var removeBtn: UIButton!
@IBOutlet weak var addBtn: UIButton!
@IBOutlet weak var hudLabel: UILabel!
lazy var shops: [Shop] = {
let path = Bundle.main.path(forResource: "shops", ofType: "plist")!
let array = NSArray(contentsOfFile: path) as! [[String: String]]
return array.map { Shop(dict: $0) }
}()
override func viewDidLoad() {
super.viewDidLoad()
hudLabel.alpha = 0
}
@IBAction func add() {
let shopWidth: CGFloat = 70
let shopHeight: CGFloat = 100
let colMargin = (shopsView.bounds.width - 3 * shopWidth) / 2
let rowMargin = colMargin
let index = shopsView.subviews.count
let row = index / 3
let col = index % 3
let shopX = CGFloat(col) * (shopWidth + colMargin)
let shopY = CGFloat(row) * (shopHeight + rowMargin)
let shopView = ShopView.shopView()
shopView.frame = CGRect(x: shopX, y: shopY, width: shopWidth, height: shopHeight)
shopView.shop = shops[index]
shopsView.addSubview(shopView)
removeBtn.isEnabled = true
addBtn.isEnabled = shopsView.subviews.count < shops.count
if !addBtn.isEnabled {
showHUD(text: "商品柜已经满了,不要再买买买了...")
}
}
@IBAction func remove() {
shopsView.subviews.last?.removeFromSuperview()
removeBtn.isEnabled = shopsView.subviews.count > 0
addBtn.isEnabled = true
if !removeBtn.isEnabled {
showHUD(text: "商品柜已经空了,继续买买买...")
}
}
private func showHUD(text: String) {
hudLabel.text = text
hudLabel.layer.removeAllAnimations()
hudLabel.alpha = 0
UIView.animate(withDuration: 0.3) {
self.hudLabel.alpha = 1
} completion: { _ in
UIView.animate(withDuration: 0.3, delay: 2.0) {
self.hudLabel.alpha = 0
}
}
}
}
底层逻辑
- 透明度动画:通过
alpha属性的渐变实现淡入淡出,UIView动画底层基于 Core Animation 的CABasicAnimation。 - 避免动画叠加:连续调用时,先
removeAllAnimations并重置alpha,否则多个动画叠加会导致显示异常。 - 非阻塞式:文本提示框不拦截用户交互,适合轻量提示。
二、系统自带提示框
1. UIAlertView(已废弃)
UIAlertView 是 iOS 8 之前的模态弹窗,iOS 8 起被废弃,由 UIAlertController 取代。新项目不应使用。
// ⚠️ 已废弃,仅作了解
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"标题"
message:@"正文"
delegate:self
cancelButtonTitle:@"取消"
otherButtonTitles:@"确定", nil];
alert.alertViewStyle = UIAlertViewStyleLoginAndPasswordInput; // 可带输入框
[alert show];
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == alertView.cancelButtonIndex) {
NSLog(@"点击了取消");
} else {
NSLog(@"点击了确定");
}
}
注意:
UIAlertView.delegate是assign(unsafe_unretained)而非 weak,若 delegate 在弹窗显示期间被释放,会导致悬垂指针崩溃。这也是其被废弃的原因之一。
2. UIActionSheet(已废弃)
UIActionSheet 用于底部弹出的多选项操作菜单,iOS 8 起同样被废弃。
// ⚠️ 已废弃,仅作了解
UIActionSheet *sheet = [[UIActionSheet alloc] initWithTitle:@"选择操作"
delegate:self
cancelButtonTitle:@"取消"
destructiveButtonTitle:@"删除"
otherButtonTitles:@"分享", @"编辑", nil];
[sheet showInView:self.view];
destructiveButtonTitle对应红色警告按钮,应用于"删除"等危险操作,而非普通的"确定"。
3. UIAlertController(推荐)
UIAlertController 是 iOS 8 引入的统一弹窗 API,同时替代了 UIAlertView 和 UIActionSheet,通过 preferredStyle 区分样式。
Alert 样式(居中弹窗)
Objective-C
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"标题"
message:@"正文"
preferredStyle:UIAlertControllerStyleAlert];
// 按钮
UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
NSLog(@"点击了确定");
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction *action) {
NSLog(@"点击了取消");
}];
UIAlertAction *deleteAction = [UIAlertAction actionWithTitle:@"删除"
style:UIAlertActionStyleDestructive
handler:^(UIAlertAction *action) {
NSLog(@"点击了删除");
}];
[alert addAction:okAction];
[alert addAction:cancelAction];
[alert addAction:deleteAction];
// 添加文本输入框(Alert 样式下最多支持 2 个输入框)
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"用户名";
}];
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = YES;
textField.placeholder = @"密码";
}];
[self presentViewController:alert animated:YES completion:nil];
Swift
let alert = UIAlertController(title: "标题", message: "正文", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default) { _ in
print("点击了确定")
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
alert.addAction(UIAlertAction(title: "删除", style: .destructive) { _ in
print("点击了删除")
})
alert.addTextField { textField in
textField.placeholder = "用户名"
}
alert.addTextField { textField in
textField.isSecureTextEntry = true
textField.placeholder = "密码"
}
present(alert, animated: true)
ActionSheet 样式(底部弹出菜单)
Objective-C
UIAlertController *sheet = [UIAlertController alertControllerWithTitle:@"选择操作"
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
[sheet addAction:[UIAlertAction actionWithTitle:@"拍照" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSLog(@"拍照");
}]];
[sheet addAction:[UIAlertAction actionWithTitle:@"从相册选择" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSLog(@"从相册选择");
}]];
[sheet addAction:[UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil]];
// iPad 上 ActionSheet 必须通过 popover 展示,否则崩溃
if (sheet.popoverPresentationController) {
sheet.popoverPresentationController.sourceView = self.view;
sheet.popoverPresentationController.sourceRect = CGRectMake(self.view.bounds.size.width / 2, self.view.bounds.size.height, 0, 0);
}
[self presentViewController:sheet animated:YES completion:nil];
Swift
let sheet = UIAlertController(title: "选择操作", message: nil, preferredStyle: .actionSheet)
sheet.addAction(UIAlertAction(title: "拍照", style: .default) { _ in print("拍照") })
sheet.addAction(UIAlertAction(title: "从相册选择", style: .default) { _ in print("从相册选择") })
sheet.addAction(UIAlertAction(title: "取消", style: .cancel))
// iPad 适配
if let popover = sheet.popoverPresentationController {
popover.sourceView = view
popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.maxY, width: 0, height: 0)
}
present(sheet, animated: true)
UIAlertAction 样式
| 样式 | 说明 |
|---|---|
UIAlertActionStyleDefault |
默认样式(蓝色文字) |
UIAlertActionStyleCancel |
取消样式(加粗文字,固定在底部/右侧) |
UIAlertActionStyleDestructive |
警告样式(红色文字,用于删除等危险操作) |
底层逻辑
- 统一控制器:
UIAlertController本质是一个UIViewController,通过presentViewController:animated:completion:模态展示,其内部管理一个UIAlertController.view。 - Block 回调:通过
handlerblock 处理按钮点击,避免了 delegate 模式的状态分散和悬垂指针问题。 - iPad 适配:ActionSheet 样式在 iPad 上必须以 popover 形式展示,需配置
popoverPresentationController.sourceView/sourceRect,否则会因缺少锚点而崩溃。 - 输入框限制:Alert 样式最多支持添加两个文本输入框(系统限制),ActionSheet 样式不支持添加输入框。
三、自定义提示框(HUD)
当系统弹窗无法满足需求时(如加载中指示器、自定义样式提示),可以自定义 HUD。
实现思路
- 创建一个覆盖全屏的半透明背景视图,拦截用户交互。
- 在中心添加
UIActivityIndicatorView(菊花)和UILabel(提示文字)。 - 提供显示和移除方法。
封装工具类
Objective-C
// HUDManager.h
#import <UIKit/UIKit.h>
@interface HUDManager : NSObject
/// 在指定视图上显示加载 HUD
+ (void)showLoadingInView:(UIView *)view text:(NSString *)text;
/// 隐藏指定视图上的 HUD
+ (void)hideInView:(UIView *)view;
/// 显示纯文本提示(自动消失)
+ (void)showText:(NSString *)text inView:(UIView *)view;
@end
// HUDManager.m
#import "HUDManager.h"
static const NSInteger kHUDTag = 99999;
@implementation HUDManager
+ (void)showLoadingInView:(UIView *)view text:(NSString *)text {
// 避免重复添加
if ([view viewWithTag:kHUDTag]) return;
// 全屏半透明背景,拦截触摸事件
UIView *hud = [[UIView alloc] initWithFrame:view.bounds];
hud.tag = kHUDTag;
hud.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.3];
hud.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
// 中心提示框
UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 150, 120)];
contentView.center = CGPointMake(hud.bounds.size.width / 2, hud.bounds.size.height / 2);
contentView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
contentView.layer.cornerRadius = 10;
contentView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin |
UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
// 菊花
UIActivityIndicatorView *activity = [[UIActivityIndicatorView alloc]
initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activity.center = CGPointMake(contentView.bounds.size.width / 2, 40);
[activity startAnimating];
[contentView addSubview:activity];
// 文字
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 75, contentView.bounds.size.width, 30)];
label.text = text;
label.textColor = [UIColor whiteColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont systemFontOfSize:14];
[contentView addSubview:label];
[hud addSubview:contentView];
[view addSubview:hud];
}
+ (void)hideInView:(UIView *)view {
UIView *hud = [view viewWithTag:kHUDTag];
[hud removeFromSuperview];
}
+ (void)showText:(NSString *)text inView:(UIView *)view {
if ([view viewWithTag:kHUDTag]) return;
UILabel *label = [[UILabel alloc] init];
label.tag = kHUDTag;
label.text = text;
label.textColor = [UIColor whiteColor];
label.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont systemFontOfSize:14];
label.numberOfLines = 0;
label.layer.cornerRadius = 8;
label.clipsToBounds = YES;
CGSize size = [text boundingRectWithSize:CGSizeMake(250, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:@{NSFontAttributeName: label.font}
context:nil].size;
label.bounds = CGRectMake(0, 0, size.width + 40, size.height + 20);
label.center = CGPointMake(view.bounds.size.width / 2, view.bounds.size.height / 2);
label.alpha = 0;
[view addSubview:label];
[UIView animateWithDuration:0.3 animations:^{
label.alpha = 1.0;
} completion:^(BOOL finished) {
[UIView animateWithDuration:0.3 delay:1.5 options:kNilOptions animations:^{
label.alpha = 0;
} completion:^(BOOL finished) {
[label removeFromSuperview];
}];
}];
}
@end
Swift
class HUDManager {
private static let hudTag = 99999
static func showLoading(in view: UIView, text: String) {
guard view.viewWithTag(hudTag) == nil else { return }
let hud = UIView(frame: view.bounds)
hud.tag = hudTag
hud.backgroundColor = UIColor.black.withAlphaComponent(0.3)
hud.autoresizingMask = [.flexibleWidth, .flexibleHeight]
let contentView = UIView(frame: CGRect(x: 0, y: 0, width: 150, height: 120))
contentView.center = CGPoint(x: hud.bounds.midX, y: hud.bounds.midY)
contentView.backgroundColor = UIColor.black.withAlphaComponent(0.7)
contentView.layer.cornerRadius = 10
contentView.autoresizingMask = [.flexibleLeftMargin, .flexibleRightMargin,
.flexibleTopMargin, .flexibleBottomMargin]
let activity = UIActivityIndicatorView(style: .large)
activity.center = CGPoint(x: contentView.bounds.midX, y: 40)
activity.startAnimating()
contentView.addSubview(activity)
let label = UILabel(frame: CGRect(x: 0, y: 75, width: contentView.bounds.width, height: 30))
label.text = text
label.textColor = .white
label.textAlignment = .center
label.font = .systemFont(ofSize: 14)
contentView.addSubview(label)
hud.addSubview(contentView)
view.addSubview(hud)
}
static func hide(in view: UIView) {
view.viewWithTag(hudTag)?.removeFromSuperview()
}
static func showText(_ text: String, in view: UIView) {
guard view.viewWithTag(hudTag) == nil else { return }
let label = UILabel()
label.tag = hudTag
label.text = text
label.textColor = .white
label.backgroundColor = UIColor.black.withAlphaComponent(0.7)
label.textAlignment = .center
label.font = .systemFont(ofSize: 14)
label.numberOfLines = 0
label.layer.cornerRadius = 8
label.clipsToBounds = true
let size = (text as NSString).boundingRect(
with: CGSize(width: 250, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: [.font: label.font!],
context: nil
).size
label.bounds = CGRect(x: 0, y: 0, width: size.width + 40, height: size.height + 20)
label.center = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
label.alpha = 0
view.addSubview(label)
UIView.animate(withDuration: 0.3) {
label.alpha = 1
} completion: { _ in
UIView.animate(withDuration: 0.3, delay: 1.5) {
label.alpha = 0
} completion: { _ in
label.removeFromSuperview()
}
}
}
}
使用示例
// 显示加载
[HUDManager showLoadingInView:self.view text:@"正在拼命加载中..."];
// 加载完成后隐藏
[HUDManager hideInView:self.view];
// 纯文本提示(自动消失)
[HUDManager showText:@"操作成功" inView:self.view];
HUDManager.showLoading(in: view, text: "正在拼命加载中...")
HUDManager.hide(in: view)
HUDManager.showText("操作成功", in: view)
主流第三方库
实际项目中通常不重复造轮子,推荐使用成熟的第三方 HUD 库:
| 库 | 特点 |
|---|---|
| MBProgressHUD | 最流行的 HUD 库,支持加载、文本、进度条、自定义视图,功能全面 |
| SVProgressHUD | 全局单例,调用极简([SVProgressHUD show]),适合全局提示 |
| JGProgressHUD | 现代化、高度可定制,支持毛玻璃效果 |
底层逻辑
- 全屏拦截:HUD 背景覆盖整个父视图并拦截触摸事件,防止用户在加载期间操作界面。
- 添加到 Window 还是 View:如果需要 HUD 不随当前视图消失(如网络请求期间用户切换页面),应添加到
UIWindow上;如果仅在当前页面有效,添加到当前view即可。 - autoresizingMask:设置合适的自动伸缩掩码,确保父视图尺寸变化(如旋转)时 HUD 仍居中覆盖。
- 避免重复添加:通过
tag检查是否已存在 HUD,防止多次调用导致叠加。
四、总结
| 类型 | 适用场景 | 是否阻塞交互 | 推荐度 |
|---|---|---|---|
| 文本提示框(UILabel) | 轻量、非阻塞的简短提示 | 否 | 适合自定义场景 |
| UIAlertView / UIActionSheet | 已废弃,仅兼容旧代码 | 是 | 不推荐 |
| UIAlertController(Alert) | 居中模态弹窗、需要用户确认 | 是 | 推荐 |
| UIAlertController(ActionSheet) | 底部多选项操作菜单 | 是 | 推荐 |
| 自定义 HUD | 加载中、进度、自定义样式提示 | 可配置 | 推荐(或用第三方库) |
- 新项目统一使用
UIAlertController,避免使用已废弃的UIAlertView/UIActionSheet。 - 加载提示优先考虑
MBProgressHUD等成熟库,减少重复开发。 - 文本提示框注意连续调用时的动画叠加问题,应在开始新动画前移除旧动画。
将来的你会感谢今天如此努力的你!
版权声明:本文为博主原创文章,未经博主允许不得转载。

浙公网安备 33010602011771号