-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathAccelerationPausingView.swift
214 lines (185 loc) · 6.23 KB
/
AccelerationPausingView.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
//
// AccelerationPausingView.swift
// FluidInterfacesSwiftUI
//
// Created by Frad LEE on 6/2/21.
//
import SwiftUI
// MARK: - AccelerationPausingView
/// To view the app switcher on iPhone X, the user swipes up from the bottom of the screen and
/// pauses midway. This interface re-creates this behavior.
///
/// # Key Features
///
/// 1. Pause is calculated based on the gesture’s acceleration.
/// 2. Faster stopping results in a faster response.
/// 3. No timers.
///
/// # Design Theory
///
/// Fluid interfaces should be fast. A delay from a timer, even if short, can make an interface feel
/// sluggish.
/// This interface is particularly cool because its reaction time is based on the user’s motion. If they
/// quickly pause, the interface quickly responds. If they slowly pause, it slowly responds.
///
/// # References
///
/// - [Building Fluid Interfaces. How to create natural gestures and…](https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5)
/// - [Calculate velocity of DragGesture](https://stackoverflow.com/questions/57222885/calculate-velocity-of-draggesture )
/// - [Gesture Deceleration - Gesture Recognizers in IOS 12, Xcode 10, and Swift 4.2](https://www.youtube.com/watch?v=cXr7ZYJXVAE)
/// - [SwiftUI Drag Gesture Tutorial](https://www.ioscreator.com/tutorials/swiftui-drag-gesture-tutorial)
struct AccelerationPausingView: View {
// MARK: Internal
var body: some View {
let drag = DragGesture()
.onChanged { value in
let offset = { () -> CGFloat in
let offset = value.translation.height
if offset > 0 {
return pow(offset, 0.7)
} else if offset < -verticalOffset * 2 {
return -verticalOffset * 2 - pow(
-(offset + verticalOffset * 2),
0.7
)
}
return offset
}()
self.currentPosition = CGSize(
width: value.translation.width + self.newPosition.width,
height: offset + self.newPosition.height
)
self.currentVelocity = CGSize(
width: value.predictedEndLocation.x - value.location.x,
height: value.predictedEndLocation.y - value.location.y
)
trackPause(velocity: currentVelocity.height, offset: offset)
}
.onEnded { _ in
hasPaused = false
withAnimation(.spring()) {
self.currentPosition = .zero
self.newPosition = .zero
}
if abs(self.currentVelocity.height) > 200.0 {
withAnimation(.spring()) {
self.currentPosition = .zero
self.newPosition = .zero
}
}
}
ZStack {
DebugView(
currentVelocityY: $currentVelocity.height,
currentOffsetY: $currentPosition.height
)
VStack {
if hasPaused {
Text("Paused").textCase(.uppercase)
.offset(x: 0, y: self.currentPosition.height)
} else {
Text("FIX").hidden()
}
RoundedRectangle(cornerRadius: 32)
.fill(
LinearGradient(
gradient: Gradient(colors: [.topColor, .bottomColor]),
startPoint: .top,
endPoint: .bottom
)
)
.frame(width: 120, height: 120, alignment: .center)
.offset(x: 0, y: self.currentPosition.height)
.gesture(drag)
}
}
}
// MARK: Private
private struct DebugView: View {
@Binding var currentVelocityY: CGFloat
@Binding var currentOffsetY: CGFloat
var body: some View {
VStack(alignment: .trailing, spacing: 8) {
Spacer()
HStack {
Text("Last Velocity:")
Spacer()
Text("\(abs(currentVelocityY), specifier: "%.2f")")
.font(.system(.body).monospacedDigit())
}
HStack {
Text("Current Offset:")
Spacer()
Text(
"\(currentOffsetY == 0 ? 0 : -currentOffsetY, specifier: "%.2f")"
)
.font(.system(.body).monospacedDigit())
}
}
.animation(nil)
.padding()
#if os(iOS)
.frame(
width: .fullScreenWidth,
height: .fullScreenWidth,
alignment: .center
)
#endif
}
}
private let verticalOffset: CGFloat = 180
/// The number of past velocities to track.
private let numberOfVelocities = 7
/// The array of past velocities.
@State private var velocities = [CGFloat]()
@State private var hasPaused = false
/// The current veloctiy of drag gesture.
///
/// The end of drag gesture velocity, but not our want:
///
/// ``` swift
/// let velocity = CGSize(
/// width: value.predictedEndLocation.x - value.location.x,
/// height: value.predictedEndLocation.y - value.location.y
/// )
/// ```
@State private var currentVelocity: CGSize = .zero
@State private var currentPosition: CGSize = .zero
@State private var newPosition: CGSize = .zero
/// Tracks the most recent velocity values, and determines whether the change is great enough to
/// be pasued.
///
/// After calling this function, the result can be checked in the `hasPaused` property.
private func trackPause(velocity: CGFloat, offset: CGFloat) {
// if the motion is paused, we are done
if hasPaused { return }
// update the array of most recent velocities
if velocities.count < numberOfVelocities {
velocities.append(velocity)
return
} else {
velocities = Array(velocities.dropFirst())
velocities.append(velocity)
}
/// Enforce minimum velocity and offset.
if abs(velocity) > 100 || abs(offset) < 50 { return }
guard let firstRecordedVelocity = velocities.first else { return }
/// If the majority of the velocity has been lost recetly, we consider the
/// motion to be paused
if abs(firstRecordedVelocity - velocity) / abs(firstRecordedVelocity) >
0.9 {
hasPaused = true
velocities.removeAll()
}
}
}
private extension Color {
static let topColor = Color(red: 0.39, green: 1.00, blue: 0.56)
static let bottomColor = Color(red: 0.32, green: 1.00, blue: 0.92)
}
// MARK: - AccelerationPausingView_Previews
struct AccelerationPausingView_Previews: PreviewProvider {
static var previews: some View {
AccelerationPausingView()
}
}