-
Notifications
You must be signed in to change notification settings - Fork 3k
/
Copy pathCollapsibleCardView.swift
221 lines (184 loc) · 9.29 KB
/
CollapsibleCardView.swift
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
import Common
import UIKit
public struct CollapsibleCardViewModel {
public let contentView: UIView
public let cardViewA11yId: String
public let title: String
public let titleA11yId: String
public let expandButtonA11yId: String
public let expandButtonA11yLabelExpanded: String
public let expandButtonA11yLabelCollapsed: String
public var expandState: CollapsibleCardView.ExpandButtonState = .collapsed
public var expandButtonA11yLabel: String {
return expandState == .expanded ? expandButtonA11yLabelExpanded : expandButtonA11yLabelCollapsed
}
// We need this init as by default the init generated by the compiler for the struct will be internal and
// can therefor not be used outside of the component library
public init(contentView: UIView,
cardViewA11yId: String,
title: String,
titleA11yId: String,
expandButtonA11yId: String,
expandButtonA11yLabelExpanded: String,
expandButtonA11yLabelCollapsed: String,
expandState: CollapsibleCardView.ExpandButtonState = .collapsed) {
self.contentView = contentView
self.cardViewA11yId = cardViewA11yId
self.title = title
self.titleA11yId = titleA11yId
self.expandButtonA11yId = expandButtonA11yId
self.expandButtonA11yLabelExpanded = expandButtonA11yLabelExpanded
self.expandButtonA11yLabelCollapsed = expandButtonA11yLabelCollapsed
self.expandState = expandState
}
}
public class CollapsibleCardView: ShadowCardView, UIGestureRecognizerDelegate {
private struct UX {
static let verticalPadding: CGFloat = 8
static let horizontalPadding: CGFloat = 8
static let titleHorizontalPadding: CGFloat = 8
static let expandButtonSize = CGSize(width: 20, height: 20)
}
public enum ExpandButtonState {
case collapsed
case expanded
var image: UIImage? {
switch self {
case .expanded:
return UIImage(named: StandardImageIdentifiers.Large.chevronUp)?.withRenderingMode(.alwaysTemplate)
case .collapsed:
return UIImage(named: StandardImageIdentifiers.Large.chevronDown)?.withRenderingMode(.alwaysTemplate)
}
}
var toggle: ExpandButtonState {
switch self {
case .expanded:
return .collapsed
case .collapsed:
return .expanded
}
}
}
// MARK: - Properties
private lazy var viewModel = CollapsibleCardViewModel(
contentView: rootView,
cardViewA11yId: "",
title: "",
titleA11yId: "",
expandButtonA11yId: "",
expandButtonA11yLabelExpanded: "",
expandButtonA11yLabelCollapsed: "",
expandState: .collapsed)
// UI
private lazy var rootView: UIView = .build { _ in }
private lazy var headerView: UIView = .build { _ in }
private lazy var containerView: UIView = .build { _ in }
private var containerHeightConstraint: NSLayoutConstraint?
private var containerBottomConstraint: NSLayoutConstraint?
private var tapRecognizer: UITapGestureRecognizer!
lazy var titleLabel: UILabel = .build { label in
label.adjustsFontForContentSizeCategory = true
label.font = DefaultDynamicFontHelper.preferredFont(withTextStyle: .headline, size: 17.0)
label.numberOfLines = 0
}
private lazy var expandButton: UIButton = .build { view in
view.setImage(self.viewModel.expandState.image, for: .normal)
view.addTarget(self, action: #selector(self.toggleExpand), for: .touchUpInside)
}
// MARK: - Inits
override init(frame: CGRect) {
super.init(frame: frame)
setupLayout()
tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHeader))
tapRecognizer.delegate = self
headerView.addGestureRecognizer(tapRecognizer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public override func configure(_ viewModel: ShadowCardViewModel) {
// the overridden method should not be used as it is lacking vital details to configure this card
fatalError("configure(:) has not been implemented.")
}
public func configure(_ viewModel: CollapsibleCardViewModel) {
self.viewModel = viewModel
containerView.subviews.forEach { $0.removeFromSuperview() }
containerView.addSubview(viewModel.contentView)
titleLabel.text = viewModel.title
titleLabel.accessibilityIdentifier = viewModel.titleA11yId
expandButton.accessibilityIdentifier = viewModel.expandButtonA11yId
expandButton.accessibilityLabel = viewModel.expandButtonA11yLabel
NSLayoutConstraint.activate([
viewModel.contentView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
viewModel.contentView.topAnchor.constraint(equalTo: containerView.topAnchor),
viewModel.contentView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
viewModel.contentView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
])
updateCardState(expandState: viewModel.expandState)
let parentViewModel = ShadowCardViewModel(view: rootView, a11yId: viewModel.cardViewA11yId)
super.configure(parentViewModel)
}
public override func applyTheme(theme: Theme) {
super.applyTheme(theme: theme)
titleLabel.textColor = theme.colors.textPrimary
expandButton.tintColor = theme.colors.iconPrimary
}
private func setupLayout() {
configure(viewModel)
headerView.addSubview(titleLabel)
headerView.addSubview(expandButton)
rootView.addSubview(headerView)
rootView.addSubview(containerView)
containerHeightConstraint = containerView.heightAnchor.constraint(equalToConstant: 0)
containerBottomConstraint = containerView.bottomAnchor.constraint(equalTo: rootView.bottomAnchor,
constant: -UX.verticalPadding)
containerBottomConstraint?.isActive = true
NSLayoutConstraint.activate([
headerView.leadingAnchor.constraint(equalTo: rootView.leadingAnchor,
constant: UX.titleHorizontalPadding),
headerView.topAnchor.constraint(equalTo: rootView.topAnchor,
constant: UX.verticalPadding),
headerView.trailingAnchor.constraint(equalTo: rootView.trailingAnchor,
constant: -UX.titleHorizontalPadding),
headerView.bottomAnchor.constraint(equalTo: containerView.topAnchor,
constant: -UX.verticalPadding),
titleLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
titleLabel.topAnchor.constraint(equalTo: headerView.topAnchor),
titleLabel.trailingAnchor.constraint(equalTo: expandButton.leadingAnchor,
constant: -UX.horizontalPadding),
titleLabel.bottomAnchor.constraint(equalTo: headerView.bottomAnchor),
titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: UX.expandButtonSize.height),
expandButton.topAnchor.constraint(greaterThanOrEqualTo: headerView.topAnchor),
expandButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor),
expandButton.bottomAnchor.constraint(lessThanOrEqualTo: headerView.bottomAnchor),
expandButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor),
expandButton.widthAnchor.constraint(equalToConstant: UX.expandButtonSize.width),
expandButton.heightAnchor.constraint(equalToConstant: UX.expandButtonSize.height),
containerView.leadingAnchor.constraint(equalTo: rootView.leadingAnchor,
constant: UX.horizontalPadding),
containerView.trailingAnchor.constraint(equalTo: rootView.trailingAnchor,
constant: -UX.horizontalPadding),
])
}
private func updateCardState(expandState: ExpandButtonState) {
let isCollapsed = expandState == .collapsed
viewModel.expandState = expandState
expandButton.setImage(viewModel.expandState.image, for: .normal)
expandButton.accessibilityLabel = viewModel.expandButtonA11yLabel
containerHeightConstraint?.isActive = isCollapsed
containerView.isHidden = isCollapsed
containerBottomConstraint?.constant = isCollapsed ? 0 : -UX.verticalPadding
UIAccessibility.post(notification: .layoutChanged, argument: nil)
}
@objc
private func toggleExpand(_ sender: UIButton) {
updateCardState(expandState: viewModel.expandState.toggle)
}
@objc
func tapHeader(_ recognizer: UITapGestureRecognizer) {
updateCardState(expandState: viewModel.expandState.toggle)
}
}