开发中我们通常在 App 长时间加载某些数据的时候需要展示 HUD 视图,告知用户当前 App 的状态。Apple 为我们提供了 UIAlertView UIActivityView 方便我们在必要的时候提醒用户。 但通常它们提供的 API 无法让我们做针对性的自定义,今天我们来实现一个简单的自定义 HUD。

首先我们先看一下在 App 中的实际运行效果。

结构非常简单: 一个灰色半透明的 UIView 和一个 UIAvtivityIndicatorView .

开始我们的工作,首先创建一个抽象类 CubeHUD, 它负责展示和隐藏 HUD 逻辑。考虑到实际开发中在很多界面下都需要展示 HUD ,所以我们直接使用单例。

1
2
3
4
5
6
import UIKit
public class CubeHUD: NSObject {
// MARK: Properties
static let shardInstance = CubeHUD()

在 swift 中完成单例非常简单, 只需上面一行代码。

接着我们完成CubeHUD中的自定义试图, 我们来实现 containerViewactivityIndicator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.black.withAlphaComponent(0.3)
return view
}()
private lazy var activitIndicator: UIActivityIndicatorView = {
let view = UIActivityIndicatorView(activityIndicatorStyle: UIActivityIndicatorViewStyle.whiteLarge)
return view
}()

我们还需要定义两个变量,isShowing 用来确保 HUD 视图同一时间只能弹出一个。dismissTimer,用来主动设置 HUD 视图消失的时间。

1
2
var isShowing = false
var dismissTimer: Timer?

之后我们来实现展示 HUD 的逻辑,showActivityIndicatorWhile(blockingUI: Bool)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class func showActivityIndicatorWhile(blockingUI: Bool = false) {
// 首先判断是否已经显示了 HUD
if self.shardInstance.isShowing {
return
}
DispatchQueue.main.async {
// 确保可以拿到 app 的 window
if
let appDelegate = UIApplication.shared.delegate as?
AppDelegate,
let window = appDelegate.window {
self.shardInstance.isShowing = true
// 设置是否需要禁止于用户交互
self.shardInstance.containerView.isUserInteractionEnabled
= blockingUI
// 配置 containerView
self.shardInstance.containerView.alpha = 0
// 将 containerView 添加到当前 window
window.addSubview(self.shardInstance.containerView)
self.shardInstance.containerView.frame = window.bounds
// 之后我们使用一个0.1秒的动画, 展示 HUD 的 containerView
springWithCompletion(duration: 0.1, animations: {
self.shardInstance.containerView.alpha = 1
}, completions: { finished in
// 在 containerView 出现之后, 我们为其添加 activityIndicator
self.shardInstance.containerView.addSubview(
self.shardInstance.activitIndicator)
self.shardInstance.activitIndicator.center =
self.shardInstance.containerView.center
self.shardInstance.indicatorLabel.center =
CGPoint(
x: self.shardInstance.containerView.center.x,
y: self.shardInstance.containerView.center.y +
25)
// 配置 activityIndicator, 下先让其缩小并隐藏
self.shardInstance.activitIndicator.startAnimating()
self.shardInstance.activitIndicator.alpha = 0
self.shardInstance.activitIndicator.transform =
CGAffineTransform.init(scaleX: 0.00001, y: 0.00001)
// 之后使用一个0.2秒的动画,让其恢复初始的大小并出现.
springWithCompletion(duration: 0.2, animations: {
self.shardInstance.activitIndicator.transform =
CGAffineTransform.init(scaleX: 1.0, y: 1.0)
self.shardInstance.activitIndicator.alpha = 1
}, completions: { finished in
self.shardInstance.activitIndicator.transform =
CGAffineTransform.identity
if let dismissTimer =
self.shardInstance.dismissTimer {
dismissTimer.invalidate()
}
// 完成展示 HUD 动画后, 我们设置计时器,
// Config.forcedHideActivityIndicatorTimeInterval
// 的值为 30 秒. 用来设置 HUD 展示的最长时间为 30 秒.
self.shardInstance.dismissTimer =
Timer(timeInterval:
Config.forcedHideActivityIndicatorTimeInterval,
target: self, selector:
#selector(CubeHUD.forcedHideActivityIndicator),
userInfo: nil, repeats: false)
})
})

接着, 我们来实现 HUD 隐藏的逻辑 hideActivityIndicator(completion: @escaping () -> Void ) 这个方法会接受一个闭包. 用来定义 HUD 隐藏结束后需要执行的任务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class func hideActivityIndicator(completion: @escaping () -> Void ){
DispatchQueue.main.async {
if self.shardInstance.isShowing {
self.shardInstance.isShowing = false
self.shardInstance.activitIndicator.transform =
CGAffineTransform.identity
self.shardInstance.indicatorLabel.transform =
CGAffineTransform.identity
springWithCompletion(duration: 0.5, animations: {
self.shardInstance.activitIndicator.transform =
CGAffineTransform.init(scaleX: 0.00001, y: 0.00001)
self.shardInstance.activitIndicator.alpha = 0
}, completions: { finished in
self.shardInstance.activitIndicator.removeFromSuperview()
springWithCompletion(duration: 0.1, animations: {
self.shardInstance.containerView.alpha = 0
}, completions: { finished in
self.shardInstance.containerView.removeFromSuperview()
completion()
})
})
}
}
}

上面的代码理解起来也非常简单, 将 activityIndicator 缩小隐藏之后, 将 containerView 从 window 上移除.

我们再定义两个接口, 让外部调用隐藏 HUD 方法时更加便捷.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 出现超过 30秒 被迫消失时, 需要执行的 alert 提醒.
class func forcedHideActivityIndicator() {
hideActivityIndicator {
if
let appDelegate = UIApplication.shared.delegate as? AppDelegate,
let _ = appDelegate.window?.rootViewController {
// Alert 超时提醒
}
}
}
// 外部调用隐藏 HUD 时使用.
class func hideActivityIndicator() {
hideActivityIndicator {
}
}

OK! Done.

我们在外部只需要使用 CubeHUD.showActivityIndicator()CubeHUD.hideActivityIndicator() 两个方法就可以展示和隐藏 HUD 视图了.