/
FloatingTweakGroupViewController.swift
359 lines (292 loc) · 11.4 KB
/
FloatingTweakGroupViewController.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
//
// FloatingTweakGroupViewController.swift
// SwiftTweaks
//
// Created by Bryan Clark on 4/6/16.
// Copyright © 2016 Khan Academy. All rights reserved.
//
import UIKit
// MARK: - FloatingTweaksWindowPresenter
internal protocol FloatingTweaksWindowPresenter {
func presentFloatingTweaksUI(forTweakGroup tweakGroup: TweakGroup)
func dismissFloatingTweaksUI()
}
// MARK: - FloatingTweakGroupViewController
/// A "floating" UI for a particular TweakGroup.
internal final class FloatingTweakGroupViewController: UIViewController {
var tweakGroup: TweakGroup? {
didSet {
titleLabel.text = tweakGroup?.title
self.tableView.reloadData()
}
}
static func editingSupported(forTweak tweak: AnyTweak) -> Bool {
switch tweak.tweakViewDataType {
case .boolean, .integer, .cgFloat, .double:
return true
case .uiColor, .stringList, .string:
return false
}
}
private let presenter: FloatingTweaksWindowPresenter
fileprivate let tweakStore: TweakStore
private let fullFrame: CGRect
internal init(frame: CGRect, tweakStore: TweakStore, presenter: FloatingTweaksWindowPresenter) {
self.tweakStore = tweakStore
self.presenter = presenter
self.fullFrame = frame
super.init(nibName: nil, bundle: nil)
view.frame = frame
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
internal var minimizedFrameOriginX: CGFloat {
return fullFrame.size.width - FloatingTweakGroupViewController.minimizedWidth + FloatingTweakGroupViewController.margins * 2
}
override func viewDidLoad() {
super.viewDidLoad()
installSubviews()
layoutSubviews()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tableView.flashScrollIndicators()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
layoutSubviews()
}
// MARK: Subviews
internal static let height: CGFloat = 168
internal static let margins: CGFloat = 5
private static let minimizedWidth: CGFloat = 30
private static let closeButtonSize = CGSize(width: 42, height: 32)
private static let navBarHeight: CGFloat = 32
private static let windowCornerRadius: CGFloat = 5
private let navBar: UIView = {
let view = UIView()
view.backgroundColor = AppTheme.Colors.floatingTweakGroupNavBG
view.layer.shadowColor = AppTheme.Shadows.floatingNavShadowColor
view.layer.shadowOpacity = AppTheme.Shadows.floatingNavShadowOpacity
view.layer.shadowOffset = AppTheme.Shadows.floatingNavShadowOffset
view.layer.shadowRadius = AppTheme.Shadows.floatingNavShadowRadius
return view
}()
private let titleLabel: UILabel = {
let label = UILabel()
label.textColor = AppTheme.Colors.sectionHeaderTitleColor
label.font = AppTheme.Fonts.sectionHeaderTitleFont
return label
}()
private let closeButton: UIButton = {
let button = UIButton()
let buttonImage = UIImage(swiftTweaksImage: .floatingCloseButton).withRenderingMode(.alwaysTemplate)
button.setImage(buttonImage.imageTintedWithColor(AppTheme.Colors.controlTinted), for: UIControlState())
button.setImage(buttonImage.imageTintedWithColor(AppTheme.Colors.controlTintedPressed), for: .highlighted)
return button
}()
fileprivate let tableView: UITableView = {
let tableView = UITableView(frame: .zero, style: .plain)
tableView.backgroundColor = .clear
tableView.register(TweakTableCell.self, forCellReuseIdentifier: FloatingTweakGroupViewController.TweakTableViewCellIdentifer)
tableView.contentInset = UIEdgeInsets(top: FloatingTweakGroupViewController.navBarHeight, left: 0, bottom: 0, right: 0)
tableView.separatorColor = AppTheme.Colors.tableSeparator
return tableView
}()
fileprivate let restoreButton: UIButton = {
let button = UIButton()
let buttonImage = UIImage(swiftTweaksImage: .floatingMinimizedArrow).withRenderingMode(.alwaysTemplate)
button.setImage(buttonImage.imageTintedWithColor(AppTheme.Colors.controlSecondary), for: UIControlState())
button.setImage(buttonImage.imageTintedWithColor(AppTheme.Colors.controlSecondaryPressed), for: .highlighted)
button.isHidden = true
return button
}()
private func installSubviews() {
// Create the rounded corners and shadows
view.layer.cornerRadius = FloatingTweakGroupViewController.windowCornerRadius
view.layer.shadowColor = AppTheme.Shadows.floatingShadowColor
view.layer.shadowOffset = AppTheme.Shadows.floatingShadowOffset
view.layer.shadowRadius = AppTheme.Shadows.floatingShadowRadius
view.layer.shadowOpacity = AppTheme.Shadows.floatingShadowOpacity
// Set up the background
view.backgroundColor = .white
// The table view
tableView.delegate = self
tableView.dataSource = self
view.addSubview(tableView)
// The "fake nav bar"
closeButton.addTarget(self, action: #selector(self.closeButtonTapped), for: .touchUpInside)
navBar.addSubview(closeButton)
navBar.addSubview(titleLabel)
view.addSubview(navBar)
// The restore button
restoreButton.addTarget(self, action: #selector(self.restore), for: .touchUpInside)
view.addSubview(restoreButton)
// The pan gesture recognizer
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.moveWindowPanGestureRecognized(_:)))
panGestureRecognizer.delegate = self
view.addGestureRecognizer(panGestureRecognizer)
}
private func layoutSubviews() {
tableView.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: FloatingTweakGroupViewController.height))
tableView.scrollIndicatorInsets = UIEdgeInsets(
top: tableView.contentInset.top,
left: 0,
bottom: 0,
right: 0
)
navBar.frame = CGRect(origin: .zero, size: CGSize(width: view.bounds.width, height: FloatingTweakGroupViewController.navBarHeight))
// Round the top two corners of the nav bar
navBar.layer.mask = {
let maskPath = UIBezierPath(
roundedRect: view.bounds,
byRoundingCorners: [.topLeft, .topRight],
cornerRadii: CGSize(
width: FloatingTweakGroupViewController.windowCornerRadius,
height: FloatingTweakGroupViewController.windowCornerRadius
)).cgPath
let mask = CAShapeLayer()
mask.path = maskPath
return mask
}()
closeButton.frame = CGRect(origin: .zero, size: FloatingTweakGroupViewController.closeButtonSize)
titleLabel.frame = CGRect(
origin: CGPoint(
x: closeButton.frame.width,
y: 0
),
size: CGSize(
width: view.bounds.width - closeButton.frame.width,
height: navBar.bounds.height
)
)
restoreButton.frame = CGRect(
origin: .zero,
size: CGSize(
width: FloatingTweakGroupViewController.minimizedWidth,
height: view.bounds.height
)
)
}
// MARK: Actions
@objc private func closeButtonTapped() {
presenter.dismissFloatingTweaksUI()
}
private static let gestureSpeedBreakpoint: CGFloat = 10
private static let gesturePositionBreakpoint: CGFloat = 30
@objc private func moveWindowPanGestureRecognized(_ gestureRecognizer: UIPanGestureRecognizer) {
switch (gestureRecognizer.state) {
case .began:
gestureRecognizer.setTranslation(self.view.frame.origin, in: self.view)
case .changed:
view.frame.origin.x = gestureRecognizer.translation(in: self.view).x
case .possible, .ended, .cancelled, .failed:
let gestureIsMovingToTheRight = (gestureRecognizer.velocity(in: nil).x > FloatingTweakGroupViewController.gestureSpeedBreakpoint)
let viewIsKindaNearTheRight = view.frame.origin.x > FloatingTweakGroupViewController.gesturePositionBreakpoint
if gestureIsMovingToTheRight && viewIsKindaNearTheRight {
minimize()
} else {
restore()
}
}
}
private static let minimizeAnimationDuration: Double = 0.3
private static let minimizeAnimationDamping: CGFloat = 0.8
private func minimize() {
// TODO map the continuous gesture's velocity into the animation.
self.restoreButton.alpha = 0
self.restoreButton.isHidden = false
UIView.animate(
withDuration: FloatingTweakGroupViewController.minimizeAnimationDuration,
delay: 0,
usingSpringWithDamping: FloatingTweakGroupViewController.minimizeAnimationDamping,
initialSpringVelocity: 0,
options: .beginFromCurrentState,
animations: {
self.view.frame.origin.x = self.minimizedFrameOriginX
self.tableView.alpha = 0
self.navBar.alpha = 0
self.restoreButton.alpha = 1
},
completion: nil
)
}
@objc private func restore() {
// TODO map the continuous gesture's velocity into the animation
UIView.animate(
withDuration: FloatingTweakGroupViewController.minimizeAnimationDuration,
delay: 0,
usingSpringWithDamping: FloatingTweakGroupViewController.minimizeAnimationDamping,
initialSpringVelocity: 0,
options: .beginFromCurrentState,
animations: {
self.view.frame.origin.x = self.fullFrame.origin.x
self.tableView.alpha = 1
self.navBar.alpha = 1
self.restoreButton.alpha = 0
},
completion: { _ in
self.restoreButton.isHidden = true
}
)
}
}
extension FloatingTweakGroupViewController: UIGestureRecognizerDelegate {
@objc func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let hitView = gestureRecognizer.view?.hitTest(gestureRecognizer.location(in: gestureRecognizer.view), with: nil) else {
return true
}
// We don't want to move the window if you're trying to drag a slider or a switch!
// But if you're dragging on the restore button, that's what we do want!
let gestureIsNotOnAControl = !hitView.isKind(of: UIControl.self)
let gestureIsOnTheRestoreButton = hitView == restoreButton
return gestureIsNotOnAControl || gestureIsOnTheRestoreButton
}
}
// MARK: Table View
extension FloatingTweakGroupViewController: UITableViewDelegate {
@objc func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let tweak = tweakAtIndexPath(indexPath) else { return }
if !FloatingTweakGroupViewController.editingSupported(forTweak: tweak) {
let alert = UIAlertController(title: "Can't edit this tweak here.", message: "You can edit it back in the main view, though!", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil))
present(alert, animated: true, completion: nil)
}
}
}
extension FloatingTweakGroupViewController: UITableViewDataSource {
fileprivate static let TweakTableViewCellIdentifer = "TweakTableViewCellIdentifer"
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tweakGroup?.tweaks.count ?? 0
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: FloatingTweakGroupViewController.TweakTableViewCellIdentifer, for: indexPath) as! TweakTableCell
let tweak = tweakAtIndexPath(indexPath)!
cell.textLabel?.text = tweak.tweakName
cell.isInFloatingTweakGroupWindow = true
cell.viewData = tweakStore.currentViewDataForTweak(tweak)
cell.delegate = self
cell.backgroundColor = .clear
cell.contentView.backgroundColor = .clear
return cell
}
fileprivate func tweakAtIndexPath(_ indexPath: IndexPath) -> AnyTweak? {
return tweakGroup?.sortedTweaks[(indexPath as NSIndexPath).row]
}
}
// MARK: TweakTableCellDelegate
extension FloatingTweakGroupViewController: TweakTableCellDelegate {
func tweakCellDidChangeCurrentValue(_ tweakCell: TweakTableCell) {
if
let indexPath = tableView.indexPath(for: tweakCell),
let viewData = tweakCell.viewData,
let tweak = tweakAtIndexPath(indexPath)
{
tweakStore.setValue(viewData, forTweak: tweak)
}
}
}