在 iOS 开发中我们会经常使用到 UIAlertController, 但本身提供的 API 接口有限, 自定义外观会非常麻烦, 更别提在其中使用自定义的 View 了. 今天我们来封装一个 ActionSheetView, 用来代替系统提供的 actionSheet 类型.

屏幕快照 2016-11-13 下午1.46.14

图中底部弹出的菜单就是我们要实现的 ActionSheetView, 它有的主体结构是由一个 UITableView 来实现, 我们可以为其定制非常多种类的 UITableViewCell, 并通过 Swift 中的枚举关联值的特性,让其外部使用变得非常简单. 比如下面这种拥有 switch 开关的 cell.

屏幕快照 2016-11-13 下午1.45.18
首先, 我们来创建一个 ActionSheetView 类, 它继承自 UIView. 我们在其中定义一个 Enum, 用来表示所以类型的 UITableViewCell.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ActionSheetView: UIView {
enum Item {
case Option(title: String, titleColor: UIColor, action: () -> Void)
case Default(title: String, titleColor: UIColor, action: () -> Bool)
case Detail(title: String, titleColor: UIColor, action: () -> Void)
case Switch(title: String, titleColor: UIColor, switchOn: Bool,
action: (_ switchOn: Bool) -> Void)
case SubtitleSwitch(title: String, titleColor: UIColor, subtitle: String,
subtitleColor: UIColor, switchOn: Bool,
action: (_ switchOn: Bool) -> Void)
case Check(title: String, titleColor: UIColor, checked: Bool,
action: () -> Void)
case Cancel
}

接着创建一个 Item 的数组, 并实现 init 方法. 我们使用外部传入的 Item 数组来进行 ActionSheetView 的配置.

1
2
3
4
5
6
var items: [Item]
init(items: [Item]) {
self.items = items
super.init(frame: CGRect.zero)
}

下面我们来创建需要使用到的 UIView.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fileprivate lazy var containerView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.clear
return view
}()
// tableView 行高
private let rowHeight: CGFloat = 60
// tableView 的高度
private var totalHeight: CGFloat {
return CGFloat(items.count) * rowHeight
}
fileprivate lazy var tableView: UITableView = {
let view = UITableView()
view.dataSource = self
view.delegate = self
view.rowHeight = self.rowHeight
view.isScrollEnabled = false
view.backgroundColor = UIColor.clear
return view
}()

上面定义的 containerView 其实算是整个 ActionSheetView 的背景视图, 与 window 大小一致. 用它来对当前屏幕的内容进行遮罩, 我们现在来定义一个 makeUI 的方法, 用来初始化 subViews 的 layout.

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
// 用来保存 tableView 的底部约束.
private var tableViewBottomConstraint: NSLayoutConstraint?
private func makeUI() {
addSubview(containerView)
containerView.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(tableView)
tableView.translatesAutoresizingMaskIntoConstraints = false
self.backgroundColor = UIColor.clear
let viewsDictionary: [String: AnyObject] = [
"containerView": containerView,
"tableView": tableView
]
let containerViewConstraintsH =
NSLayoutConstraint.constraints(withVisualFormat: "H:|[containerView]|",
options: [], metrics: nil, views: viewsDictionary)
let containerViewConstraintsV =
NSLayoutConstraint.constraints(withVisualFormat: "V:|[containerView]|",
options: [], metrics: nil, views: viewsDictionary)
NSLayoutConstraint.activate(containerViewConstraintsH)
NSLayoutConstraint.activate(containerViewConstraintsV)
let tableViewConstraintsH =
NSLayoutConstraint.constraints(withVisualFormat: "H:|[tableView]|",
options: [], metrics: nil, views: viewsDictionary)
let tableViewBottomConstraint = NSLayoutConstraint(item: tableView,
attribute: .bottom, relatedBy: .equal, toItem: containerView,
attribute: .bottom, multiplier: 1.0, constant: self.totalHeight)
self.tableViewBottomConstraint = tableViewBottomConstraint
let tableViewHeightConstraint = NSLayoutConstraint(item: tableView,
attribute: .height, relatedBy: .equal, toItem: nil,
attribute: .notAnAttribute, multiplier: 1.0, constant: self.totalHeight)
NSLayoutConstraint.activate(tableViewConstraintsH)
NSLayoutConstraint.activate([tableViewBottomConstraint,
tableViewHeightConstraint])
}

上面的代码用 containerView 填充了整个屏幕, 下面为其添加一个点击事件, 用于隐藏 ActionSheetView. 我们将把这些代码写到 didMoveToSuperView 中, 并在此执行 makeUI 的初始化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private var isFirstTimeBeenAddedAsSubview = true
override func didMoveToSuperview() {
super.didMoveToSuperview()
if isFirstTimeBeenAddedAsSubview {
isFirstTimeBeenAddedAsSubview = false
makeUI()
let tap = UITapGestureRecognizer(target: self, action:
#selector(ActionSheetView.hide))
containerView.addGestureRecognizer(tap)
tap.cancelsTouchesInView = true
tap.delegate = self
}
}

现在我们来实现 ActionSheetViewshowhide 方法. 我们利用简单的动画让弹出的显得自然, 并利用之前存储的 tableViewBottomConstraint 属性来控制 ActionSheetView 的消失.

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
func show(in view: UIView) {
frame = view.bounds
view.addSubview(self)
layoutIfNeeded()
containerView.alpha = 1
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn,
animations: { _ in
self.containerView.backgroundColor =
UIColor.black.withAlphaComponent(0.3)
}, completion: { _ in
})
UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut,
animations: { _ in
self.tableViewBottomConstraint?.constant = 0
self.layoutIfNeeded()
}, completion: { _ in
})
}
func hide() {
UIView.animate(withDuration: 0.2, delay: 0.0, options: .curveEaseIn,
animations: { _ in
self.tableViewBottomConstraint?.constant = self.totalHeight
self.layoutIfNeeded()
}, completion: { _ in
})
UIView.animate(withDuration: 0.2, delay: 0.1, options: .curveEaseOut,
animations: { _ in
self.containerView.backgroundColor = UIColor.clear
}, completion: { _ in
self.removeFromSuperview()
})
}

最后我们来完成 ActionSheetViewtableView 的 delegate & dataSource.

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
extension ActionSheetView: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int)
-> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let item = items[indexPath.row]
switch item {
case let .Option(title, titleColor, action):
let cell = tableView.dequeueReusableCell(withIdentifier:
ActionSheetOptionCell.reuseIdentifier, for: indexPath) as!
ActionSheetOptionCell
cell.colorTitlelabel.text = title
cell.colorTitleLabeltextColor = titleColor
cell.action = action
return cell
case let .Default(title, titleColor, _):
let cell = tableView.dequeueReusableCell(withIdentifier:
ActionSheetDefaultCell.reuseIdentifier) as! ActionSheetDefaultCell
cell.colorTitlelabel.text = title
cell.colorTitleLabeltextColor = titleColor
return cell
case let .Detail(title, titleColor, action):
let cell = tableView.dequeueReusableCell(withIdentifier:
ActionSheetDetailCell.reuseIdentifier) as! ActionSheetDetailCell
cell.textLabel?.text = title
cell.textLabel?.textColor = titleColor
cell.action = action
return cell
case let .Switch(title, titleColor, switchOn, action):
let cell = tableView.dequeueReusableCell(withIdentifier:
ActionSheetSwitchCell.reuseIdentifier) as! ActionSheetSwitchCell
cell.textLabel?.text = title
cell.textLabel?.textColor = titleColor
cell.checkedSwitch.isOn = switchOn
cell.action = action
return cell
case let .SubtitleSwitch(title, titleColor, subtitle, subtitleColor,
switchOn, action):
let cell = tableView.dequeueReusableCell(withIdentifier:
ActionSheetSubtitleSwitchCell.reuseIdentifier) as! ActionSheetSubtitleSwitchCell
cell.titleLabel.text = title
cell.titleLabel.textColor = titleColor
cell.subtitleLabel.text = subtitle
cell.subtitleLabel.textColor = subtitleColor
cell.checkedSwitch.isOn = switchOn
cell.action = action
return cell
case let .Check(title, titleColor, checked, _):
let cell = tableView.dequeueReusableCell(withIdentifier:
ActionSheetCheckCell.reuseIdentifier) as! ActionSheetCheckCell
cell.colorTitleLabel.text = title
cell.colorTitleLabelTextColor = titleColor
cell.checkImageView.isHidden = !checked
return cell
case .Cancel:
let cell = tableView.dequeueReusableCell(withIdentifier:
ActionSheetDefaultCell.reuseIdentifier) as! ActionSheetDefaultCell
cell.colorTitlelabel.text = NSLocalizedString("取消", comment: "")
cell.colorTitleLabeltextColor = UIColor.cubeTintColor()
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
defer {
tableView.deselectRow(at: indexPath as IndexPath, animated: true)
}
let item = items[indexPath.row]
switch item {
case .Option(_, _, let action):
hideAndDo(afterHideAction: action)
case .Default(_, _, let action):
if action() {
hide()
}
case .Detail(_, _, let action):
hideAndDo(afterHideAction: action)
case .Switch:
break
case .SubtitleSwitch:
break
case .Check(_, _, _, let action):
action()
hide()
case .Cancel:
hide()
break
}
}

以上代码的逻辑非常简单, 根据传入的 Items 实例, 为每行分配合适的 UITableViewCell , 并对 cell 进行配置. 在 tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) 方法中, 我们在这里执行 Item 枚举所关联的 action.

另外 tableView 所使用的 cell 的实现就不一一列举了, 仅贴出 ActionSheetSwitchCell 的实现好了, 其他都大同小异.

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
private class ActionSheetSwitchCell: UITableViewCell {
class var reuseIdentifier: String {
return "\(self)"
}
var action: ((Bool) -> Void)?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
layoutMargins = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)
textLabel?.textColor = UIColor.darkGray
textLabel?.font = UIFont.systemFont(ofSize: 18, weight: UIFontWeightLight)
makeUI()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
lazy var checkedSwitch: UISwitch = {
let s = UISwitch()
s.addTarget(self, action:
#selector(ActionSheetSwitchCell.toggleSwitch(_:)), for: .valueChanged)
return s
}()
@objc func toggleSwitch(_ sender: UISwitch) {
action?(sender.isOn)
}
func makeUI() {
contentView.addSubview(checkedSwitch)
checkedSwitch.translatesAutoresizingMaskIntoConstraints = false
let centerY = NSLayoutConstraint(item: checkedSwitch, attribute: .centerY,
relatedBy: .equal, toItem: contentView, attribute: .centerY, multiplier:
1, constant: 0)
let trailing = NSLayoutConstraint(item: checkedSwitch,
attribute: .trailing, relatedBy: .equal, toItem: contentView,
attribute: .trailing, multiplier: 1, constant: -20)
NSLayoutConstraint.activate([centerY, trailing])
}
}

OK! Done.
我们最后来看一下使用 ActinoSheetView 的代码片段.

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
class ViewController: UIViewController {
private lazy var actionSheetView: ActionSheetView = {
var items = self.creatActionSheetViewItems()
items.append(ActionSheetView.Item.Cancel)
let sheetView = ActionSheetView(items: items)
return sheetView
}()
private func creatActionSheetViewItems() -> [ActionSheetView.Item] {
let switchItem = ActionSheetView.Item.Switch(
title: "同步",
titleColor: UIColor.cubeTintColor(),
switchOn: true, action: { [weak self] switchOn in
})
let editItem = ActionSheetView.Item.Option(
title: "编辑此公式",
titleColor: UIColor.cubeTintColor(),
action: { [weak self] in
guard let strongSelf = self else { return }
})
let deleteItem = ActionSheetView.Item.Option(
title: "删除此公式",
titleColor: UIColor.red,
action: { [weak self] in
guard let strongSelf = self else { return }
})
let copyToMy = ActionSheetView.Item.Option(
title: "编辑并添加到公式",
titleColor: UIColor.cubeTintColor(),
action: { [weak self] in
guard let strongSelf = self else { return }
})
return [switchItem, editItem, deleteItem, copyToMy]
}
@IBAction func showActionSheetView(_ sender: AnyObject) {
if let window = view.window {
actionSheetView.showInView(view: window)
}
}
}

之后如何希望实现更多种类的 UITableViewCell 只需要对 Item 这个枚举增加新的 case 就可以了.