-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathCalculatorButtonView.swift
220 lines (200 loc) · 6.47 KB
/
CalculatorButtonView.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
//
// CalculatorButtonView.swift
// FluidInterfacesSwiftUI
//
// Created by Frad LEE on 5/27/21.
//
import SwiftUI
// MARK: - CalculatorButtonView
/// A iOS calculator app like button.
///
/// # Key Features
///
/// 1. Highlights instantly on touch.
/// 2. Can be tapped rapidly even when mid-animation.
/// 3. User can touch down and drag outside of the button to cancel the tap.
/// 4. User can touch down, drag outside, drag back in, and confirm the tap.
///
/// # Design Theory
///
/// We want buttons that feel responsive, acknowledging to the user that they are functional. In
/// addition, we want the action to be cancellable if the user decides against their action after they
/// touched down. This allows users to make quicker decisions since they can perform actions in
/// parallel with thought.
///
/// # `Button` in `SwiftUI`
///
/// In `UIKit` you can use `touchDown`, `touchDragEnter` or some other useful
/// `[UIControl.Event](https://developer.apple.com/documentation/uikit/uicontrol/event )`,
/// in `SwiftUI` you needs to customize these gestures.
///
/// But very lucky, the default gesture of `Button` in `SwiftUI` looks match the features we want,
/// all we need is a customized `ButtonStyle`.
///
/// # References
///
/// - [Composing SwiftUI Gestures](https://developer.apple.com/documentation/swiftui/composing-swiftui-gestures)
/// - [Mastering buttons in SwiftUI](https://swiftwithmajid.com/2020/02/19/mastering-buttons-in-swiftui/)
/// - [Building Fluid Interfaces. How to create natural gestures and…](https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5)
/// - [Designing Fluid Interfaces ](https://developer.apple.com/videos/play/wwdc2018/803/?time=3013)
/// - [How to set custom highlighted state of SwiftUI Button](https://stackoverflow.com/a/56980172/3413981 )
///
struct CalculatorButtonView: View {
// MARK: Internal
var body: some View {
VStack(spacing: 32) {
HeaderView(
title: "Simultaneous",
description: "同时进行手势,类似 Apple 的计算器 app。"
)
Button(action: {
print("Button style 1 tapped.")
}) {
Text("9")
}
.buttonStyle(CalculatorButtonStyle1())
Spacer().frame(height: 32)
HeaderView(
title: "Sequenced",
description: "循序渐进手势,依次识别点击、长按、拖拽,仅供娱乐。"
)
Button(action: {
print("Button style 2 tapped")
}) {
Text("9")
}
.buttonStyle(CalculatorButtonStyle2())
}
.padding()
.fullScreenBlackBackgroundIgnoresSafeArea()
}
// MARK: Private
private struct HeaderView: View {
@State var title: String
@State var description: String
var body: some View {
VStack(alignment: .leading, spacing: 8.0) {
HStack {
Text(title)
.font(.title)
.textCase(.uppercase)
Spacer()
}
Text(description)
}
.foregroundColor(.white)
}
}
}
// MARK: - CalculatorButtonStyle1
struct CalculatorButtonStyle1: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
configuration.label
.foregroundColor(.white)
.font(.largeTitle)
.padding(32)
.background(
Circle()
.foregroundColor(configuration.isPressed ?
.highlightedButtonColor : .normalButtonColor)
.animation(configuration.isPressed ?
nil : .easeOut(duration: 0.5))
)
.cornerRadius(8.0)
}
}
// MARK: - CalculatorButtonStyle2
/// `PrimitiveButtonStyle` protocol that looks very similar to `ButtonStyle` but provides all
/// the needed API to build a super custom button.
///
/// Read [this link](https://swiftwithmajid.com/2020/02/19/mastering-buttons-in-swiftui/ )
/// for more.
/// - Attention: When `PrimitiveButtonStyle` used in `buttonStyle(_:)`, all the defualt
/// `Button` styles will be **ignored**.
struct CalculatorButtonStyle2: PrimitiveButtonStyle {
/// Model sequenced gesture states.
enum DragState {
case inactive
case pressing
case dragging(translation: CGSize)
// MARK: Internal
var translation: CGSize {
switch self {
case .inactive, .pressing:
return .zero
case let .dragging(translation):
return translation
}
}
var isActive: Bool {
switch self {
case .inactive:
return false
case .dragging, .pressing:
return true
}
}
var isDragging: Bool {
switch self {
case .inactive, .pressing:
return false
case .dragging:
return true
}
}
}
@GestureState var dragState = DragState.inactive
@State var viewState = CGSize.zero
func makeBody(configuration: Configuration) -> some View {
let minimumLongPressDuration = 0.5
let longPressDrag =
LongPressGesture(minimumDuration: minimumLongPressDuration)
.sequenced(before: DragGesture())
.updating($dragState) { value, state, _ in
switch value {
// Long press begins.
case .first(true):
state = .pressing
// Long press confirmed, dragging may begin.
case .second(true, let drag):
state = .dragging(translation: drag?.translation ?? .zero)
// Dragging ended or the long press cancelled.
default:
state = .inactive
}
}
.onEnded { value in
guard case .second(true, let drag?) = value else { return }
self.viewState.width += drag.translation.width
self.viewState.height += drag.translation.height
}
configuration.label
.foregroundColor(.white)
.font(.largeTitle)
.padding(32)
.background(
GeometryReader { _ in
Circle()
.foregroundColor(dragState.isActive ?
.highlightedButtonColor : .normalButtonColor)
.animation(dragState.isActive ?
nil : .easeOut(duration: minimumLongPressDuration))
.overlay(dragState.isDragging ? Circle()
.stroke(Color.white, lineWidth: 2) : nil)
}
)
.offset(
x: viewState.width + dragState.translation.width,
y: viewState.height + dragState.translation.height
)
.animation(.default)
.gesture(longPressDrag)
}
}
// MARK: - CalculatorButtonView_Previews
struct CalculatorButtonView_Previews: PreviewProvider {
static var previews: some View {
CalculatorButtonView()
.fullScreenBlackBackgroundIgnoresSafeArea()
}
}