-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathRewardingMomentumView.swift
134 lines (118 loc) · 3.52 KB
/
RewardingMomentumView.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
//
// RewardingMomentumView.swift
// FluidInterfacesSwiftUI
//
// Created by lixi on 6/2/21.
//
import SwiftUI
// MARK: - RewardingMomentumView
/// A drawer with open and closed states that has bounciness based on the velocity of the gesture.
///
/// # Key Features
///
/// 1. Tapping the drawer opens it without bounciness.
/// 2. Flicking the drawer opens it with bounciness.
/// 3. Interactive, interruptible, and reversible.
///
/// # Design Theory
///
/// This drawer shows the concept of rewarding momentum. When the user swipes a view with
/// velocity, it’s much more satisfying to animate the view with bounciness. This makes the interface
/// feel alive and fun.
///
/// When the drawer is tapped, it animates without bounciness, which feels appropriate, since a tap
/// has no momentum in a particular direction.
///
/// When designing custom interactions, it’s important to remember that interfaces can have
/// different animations for different interactions.
///
/// # References
///
/// - [Building Fluid Interfaces. How to create natural gestures and…](https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5)
struct RewardingMomentumView: View {
// MARK: Internal
var body: some View {
let tap = TapGesture()
.onEnded {
togglePositionY()
withAnimation(.default) { self.currentPositionY = self.newPositionY }
}
let drag = DragGesture()
.onChanged { value in
withAnimation(.default) {
self.currentPositionY =
value.translation.height + self.newPositionY
}
}
.onEnded { value in
let offsetY = value.translation.height
if offsetY > 100 { isActived = true } else { isActived = false }
togglePositionY()
withAnimation(.spring()) { self.currentPositionY = self.newPositionY }
}
ZStack {
heroView
.gesture(drag)
.gesture(tap)
debugView
}
}
// MARK: Private
@State private var isActived = false
@State private var currentPositionY: CGFloat = .fullScreenHeight * 0.68
@State private var newPositionY: CGFloat = .zero
private func togglePositionY() {
isActived.toggle()
newPositionY = isActived ?
.fullScreenHeight * 0.1 : .fullScreenHeight * 0.68
}
}
extension RewardingMomentumView {
private var heroView: some View {
ZStack {
RoundedRectangle(cornerRadius: 32)
.fill(
LinearGradient(
gradient: Gradient(colors: [.topColor, .bottomColor]),
startPoint: .top,
endPoint: .bottom
)
)
VStack {
RoundedRectangle(cornerRadius: 4)
.frame(width: 64, height: 8, alignment: .center)
.foregroundColor(.white.opacity(0.7))
.padding()
Spacer()
}
}
.offset(y: currentPositionY)
}
private var debugView: some View {
VStack {
Spacer()
HStack(spacing: 32) {
Text("Current Position:").textCase(.uppercase)
Spacer()
FormatedNumView(num: $currentPositionY)
}
.padding()
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.white.opacity(0.7))
)
}
.offset(y: 16.0)
.padding()
}
}
private extension Color {
static let topColor = Color(red: 0.38, green: 0.66, blue: 1.00)
static let bottomColor = Color(red: 0.14, green: 0.23, blue: 0.82)
}
// MARK: - RewardingMomentumView_Previews
struct RewardingMomentumView_Previews: PreviewProvider {
static var previews: some View {
RewardingMomentumView()
}
}