Skip to content
This repository was archived by the owner on Sep 20, 2023. It is now read-only.

Commit 2e908a4

Browse files
authored
Better button UX and animations (#2578)
* add new control for button FX * better API and defaults, wire up login * add fx to navigation controls * touch fx on hover button * fx on the merge button (req small refactor) * tweak manage button
1 parent ab31975 commit 2e908a4

File tree

11 files changed

+151
-50
lines changed

11 files changed

+151
-50
lines changed

Classes/History/PathHistoryViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ BaseListViewControllerDataSource {
3636
subtitle: viewModel.path?.path,
3737
chevronEnabled: false
3838
)
39+
titleView.addTouchEffect()
3940
navigationItem.titleView = titleView
4041
}
4142

Classes/Issues/IssueManagingContextController.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ final class IssueManagingContextController: NSObject, ContextMenuDelegate {
5656

5757
init(model: IssueDetailsModel, client: GithubClient) {
5858
let button = IssueManageButton()
59+
// alpha animation is jarring when you see the contents beneath
60+
button.addTouchEffect(UIControlEffect(
61+
backgroundColor: "3E93F4".color,
62+
alpha: 1,
63+
transform: CGAffineTransform(scaleX: 0.92, y: 0.92)
64+
))
65+
button.adjustsImageWhenHighlighted = false
5966
manageButton = button
6067
self.client = client
6168
self.model = model

Classes/Issues/IssuesViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ IssueManagingContextControllerDelegate {
146146
let labelString = String(format: labelFormat, arguments: [model.number, model.repo, model.owner])
147147

148148
let navigationTitle = DropdownTitleView()
149+
navigationTitle.addTouchEffect()
149150
navigationTitle.addTarget(self, action: #selector(onNavigationTitle(sender:)), for: .touchUpInside)
150151
navigationTitle.configure(
151152
title: "#\(model.number)",

Classes/Issues/Merge/IssueMergeSectionController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ ListBindingSectionControllerSelectionDelegate {
212212
title: NSLocalizedString("Change merge type", comment: ""),
213213
preferredStyle: .actionSheet
214214
)
215-
alert.popoverPresentationController?.sourceView = button.optionIconView
215+
alert.popoverPresentationController?.sourceView = button.optionButton
216216

217217
for type in types {
218218
alert.add(action: AlertAction(AlertActionBuilder {

Classes/Issues/Merge/MergeButton.swift

Lines changed: 32 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ protocol MergeButtonDelegate: class {
1414
func didSelectOptions(button: MergeButton)
1515
}
1616

17-
final class MergeButton: UIView {
17+
final class MergeButton: UIControl {
1818

1919
weak var delegate: MergeButtonDelegate?
2020

2121
// public to use as source view for popover
22-
let optionIconView = UIImageView()
22+
let optionButton = UIButton()
2323

2424
private let mergeLabel = UILabel()
2525
private let optionBorder = UIView()
@@ -29,20 +29,30 @@ final class MergeButton: UIView {
2929
override init(frame: CGRect) {
3030
super.init(frame: frame)
3131

32+
addTouchEffect()
33+
3234
addSubview(mergeLabel)
33-
addSubview(optionIconView)
35+
addSubview(optionButton)
3436
addSubview(optionBorder)
3537
addSubview(activityView)
3638

39+
addTarget(self, action: #selector(onMainTouch), for: .touchUpInside)
3740
layer.cornerRadius = Styles.Sizes.avatarCornerRadius
3841

3942
let image = UIImage(named: "chevron-down").withRenderingMode(.alwaysTemplate)
40-
let optionButtonWidth = (image.size.width ?? 0) + (2 * Styles.Sizes.gutter)
41-
optionIconView.contentMode = .center
42-
optionIconView.image = image
43-
optionIconView.isAccessibilityElement = true
44-
optionIconView.accessibilityTraits = UIAccessibilityTraitButton
45-
optionIconView.snp.makeConstraints { make in
43+
let optionButtonWidth = image.size.width + 2 * Styles.Sizes.gutter
44+
// more exagerated than the default given the small button size
45+
optionButton.addTouchEffect(UIControlEffect(
46+
alpha: 0.5,
47+
transform: CGAffineTransform(scaleX: 0.85, y: 0.85)
48+
))
49+
optionButton.adjustsImageWhenHighlighted = false
50+
optionButton.imageView?.contentMode = .center
51+
optionButton.setImage(image, for: .normal)
52+
optionButton.isAccessibilityElement = true
53+
optionButton.accessibilityTraits = UIAccessibilityTraitButton
54+
optionButton.addTarget(self, action: #selector(onOptionsTouch), for: .touchUpInside)
55+
optionButton.snp.makeConstraints { make in
4656
make.top.right.bottom.equalToSuperview()
4757
make.width.equalTo(optionButtonWidth)
4858
}
@@ -58,16 +68,14 @@ final class MergeButton: UIView {
5868
make.top.equalToSuperview().offset(Styles.Sizes.rowSpacing/2)
5969
make.bottom.equalToSuperview().offset(-Styles.Sizes.rowSpacing/2)
6070
make.width.equalTo(1/UIScreen.main.scale)
61-
make.right.equalTo(optionIconView.snp.left)
71+
make.right.equalTo(optionButton.snp.left)
6272
}
6373

6474
activityView.hidesWhenStopped = true
6575
activityView.snp.makeConstraints { make in
6676
make.centerY.equalToSuperview()
6777
make.right.equalTo(mergeLabel.snp.left).offset(-Styles.Sizes.columnSpacing)
6878
}
69-
70-
addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap(recognizer:))))
7179
}
7280

7381
required init?(coder aDecoder: NSCoder) {
@@ -99,7 +107,7 @@ final class MergeButton: UIView {
99107
]
100108
layer.addSublayer(gradientLayer)
101109

102-
[mergeLabel, optionIconView, optionBorder, activityView].forEach {
110+
[mergeLabel, optionButton, optionBorder, activityView].forEach {
103111
bringSubview(toFront: $0)
104112
}
105113
} else {
@@ -108,7 +116,7 @@ final class MergeButton: UIView {
108116

109117
let titleColor = enabled ? .white : Styles.Colors.Gray.dark.color
110118
mergeLabel.textColor = titleColor
111-
optionIconView.tintColor = titleColor
119+
optionButton.imageView?.tintColor = titleColor
112120
optionBorder.backgroundColor = titleColor
113121

114122
mergeLabel.text = title
@@ -120,29 +128,11 @@ final class MergeButton: UIView {
120128
}
121129

122130
let mergeButtonElement = mergeElement(withAccessibilityLabel: title)
123-
accessibilityElements = [mergeButtonElement, optionIconView]
124-
optionIconView.accessibilityLabel = NSLocalizedString("More merging options", comment: "More options button for merging")
125-
}
126-
127-
// MARK: Overrides
128-
129-
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
130-
super.touchesBegan(touches, with: event)
131-
// don't highlight when touching inside the chevron button
132-
if let touch = touches.first, optionIconView.frame.contains(touch.location(in: self)) {
133-
return
134-
}
135-
highlight(true)
136-
}
137-
138-
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
139-
super.touchesCancelled(touches, with: event)
140-
highlight(false)
141-
}
142-
143-
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
144-
super.touchesEnded(touches, with: event)
145-
highlight(false)
131+
accessibilityElements = [mergeButtonElement, optionButton]
132+
optionButton.accessibilityLabel = NSLocalizedString(
133+
"More merging options",
134+
comment: "More options button for merging"
135+
)
146136
}
147137

148138
// MARK: Private API
@@ -153,27 +143,20 @@ final class MergeButton: UIView {
153143
element.accessibilityFrameInContainerSpace = CGRect(
154144
origin: bounds.origin,
155145
size: CGSize(
156-
width: bounds.size.width - optionIconView.bounds.size.width,
146+
width: bounds.size.width - optionButton.bounds.size.width,
157147
height: bounds.size.height
158148
)
159149
)
160150
element.accessibilityTraits |= UIAccessibilityTraitButton
161151
return element
162152
}
163153

164-
func highlight(_ highlight: Bool) {
165-
guard isUserInteractionEnabled else { return }
166-
alpha = highlight ? 0.5 : 1
154+
@objc func onMainTouch() {
155+
delegate?.didSelect(button: self)
167156
}
168157

169-
@objc func onTap(recognizer: UITapGestureRecognizer) {
170-
guard recognizer.state == .ended else { return }
171-
172-
if optionIconView.frame.contains(recognizer.location(in: self)) {
173-
delegate?.didSelectOptions(button: self)
174-
} else {
175-
delegate?.didSelect(button: self)
176-
}
158+
@objc func onOptionsTouch() {
159+
delegate?.didSelectOptions(button: self)
177160
}
178161

179162
}

Classes/Login/LoginSplashViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ final class LoginSplashViewController: UIViewController {
6868
super.viewDidLoad()
6969
state = .idle
7070
signInButton.layer.cornerRadius = Styles.Sizes.cardCornerRadius
71+
signInButton.addTouchEffect()
7172
}
7273

7374
override func viewDidAppear(_ animated: Bool) {

Classes/Notifications/NotificationsViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ InboxFilterControllerListener {
5959

6060
navigationController?.tabBarItem.badgeColor = Styles.Colors.Red.medium.color
6161
navigationItem.titleView = navigationTitle
62+
navigationTitle.addTouchEffect()
6263
navigationTitle.titleFont = UIFont.preferredFont(forTextStyle: .headline)
6364
navigationTitle.addTarget(self, action: #selector(onNavigationTitle), for: .touchUpInside)
6465

Classes/Repository/RepositoryViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ EmptyViewDelegate {
8787
updateRightBarItems()
8888

8989
let navigationTitle = DropdownTitleView()
90+
navigationTitle.addTouchEffect()
9091
navigationItem.titleView = navigationTitle
9192
navigationTitle.addTarget(self, action: #selector(onNavigationTitle(sender:)), for: .touchUpInside)
9293
let labelFormat = NSLocalizedString(

Classes/View Controllers/UIViewController+FilePathTitle.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extension UIViewController {
3434

3535
if let title = filePath.current {
3636
let navigationTitle = DropdownTitleView()
37+
navigationTitle.addTouchEffect()
3738
navigationTitle.configure(
3839
title: title,
3940
subtitle: filePath.basePath,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// UIControlEffects.swift
3+
// Freetime
4+
//
5+
// Created by Ryan Nystrom on 1/1/19.
6+
// Copyright © 2019 Ryan Nystrom. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
struct UIControlEffect {
12+
let backgroundColor: UIColor?
13+
let alpha: CGFloat
14+
let transform: CGAffineTransform
15+
let downDuration: TimeInterval
16+
let liftDuration: TimeInterval
17+
18+
init(
19+
backgroundColor: UIColor? = nil,
20+
alpha: CGFloat = 0.8,
21+
transform: CGAffineTransform = CGAffineTransform(scaleX: 0.97, y: 0.97),
22+
downDuration: TimeInterval = 0.07,
23+
liftDuration: TimeInterval = 0.13
24+
) {
25+
self.backgroundColor = backgroundColor
26+
self.alpha = alpha
27+
self.transform = transform
28+
self.downDuration = downDuration
29+
self.liftDuration = liftDuration
30+
}
31+
}
32+
33+
fileprivate final class UIControlEffects: NSObject {
34+
35+
struct State {
36+
let backgroundColor: UIColor?
37+
let alpha: CGFloat
38+
let transform: CGAffineTransform
39+
}
40+
41+
let effect: UIControlEffect
42+
var savedState: State?
43+
44+
init(effect: UIControlEffect) {
45+
self.effect = effect
46+
}
47+
48+
@objc func didTouchDown(_ control: UIControl) {
49+
// restore when lifted
50+
savedState = State(
51+
backgroundColor:
52+
control.backgroundColor,
53+
alpha: control.alpha,
54+
transform: control.transform
55+
)
56+
57+
let effect = self.effect
58+
UIView.animate(withDuration: effect.downDuration) {
59+
if let backgroundColor = effect.backgroundColor {
60+
control.backgroundColor = backgroundColor
61+
}
62+
control.alpha = effect.alpha
63+
control.transform = effect.transform
64+
}
65+
}
66+
67+
@objc func didLift(_ control: UIControl) {
68+
guard let savedState = self.savedState else { return }
69+
UIView.animate(withDuration: effect.liftDuration) {
70+
control.backgroundColor = savedState.backgroundColor
71+
control.alpha = savedState.alpha
72+
control.transform = savedState.transform
73+
}
74+
}
75+
76+
}
77+
78+
extension UIControl {
79+
80+
private struct AssociatedKeys {
81+
static var Name = "com.freetime.uicontroleffects.key"
82+
}
83+
84+
func addTouchEffect(_ effect: UIControlEffect = UIControlEffect()) {
85+
let object = UIControlEffects(effect: effect)
86+
87+
addTarget(object, action: #selector(object.didTouchDown), for: .touchDown)
88+
addTarget(object, action: #selector(object.didLift), for: .touchUpInside)
89+
addTarget(object, action: #selector(object.didLift), for: .touchUpOutside)
90+
addTarget(object, action: #selector(object.didLift), for: .touchCancel)
91+
92+
// bind the effect object's lifecycle to the control
93+
objc_setAssociatedObject(
94+
self,
95+
&AssociatedKeys.Name,
96+
object,
97+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
98+
)
99+
}
100+
101+
}

0 commit comments

Comments
 (0)