-
Notifications
You must be signed in to change notification settings - Fork 2
/
FlashlightButtonView.swift
141 lines (126 loc) · 4.16 KB
/
FlashlightButtonView.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
//
// FlashlightButtonView.swift
// FluidInterfacesSwiftUI
//
// Created by Frad LEE on 5/31/21.
//
import SwiftUI
// MARK: - FlashlightButtonView
/// A iOS like flashlight button.
///
/// # Key Features
///
/// 1. Requires an intentional gesture with `LongPressGesture`, [see more](https://developer.apple.com/documentation/swiftui/longpressgesture ).
/// 2. Bounciness hints at the required gesture.
/// 3. Haptic feedback confirms activation.
///
/// # Design Theory
///
/// Apple wanted to create a button that was easily and quickly accessible, but couldn’t be triggered
/// accidentally. Requiring long press to activate the flashlight is a great choice, but lacks
/// affordance and feedback.
///
/// In order to solve those problems, the button is springy and grows as the user applies force,
/// hinting at the required gesture. In addition, there are two separate vibrations of haptic feedback:
/// one when the required amount of force is applied, and another when the button activates as the
/// force is reduced. These haptics mimic the behavior of a physical button.
///
/// - Note:
/// The [origin version](https://github.com/nathangitter/fluid-interfaces/blob/master/FluidInterfaces/FluidInterfaces/FlashlightButton.swift)
/// calls [3D Touch](https://developer.apple.com/design/human-interface-guidelines/ios/user-interaction/3d-touch/)
/// which has been deprecated, in this version it will replaced by `LongPressGesture` and
/// `DragGesture`.
///
/// # References
///
/// - [Building Fluid Interfaces. How to create natural gestures and…](https://medium.com/@nathangitter/building-fluid-interfaces-ios-swift-9732bb934bf5)
/// - [Composing SwiftUI Gestures](https://developer.apple.com/documentation/swiftui/composing-swiftui-gestures)
struct FlashlightButtonView: View {
// MARK: Internal
var body: some View {
let minimumLongPressDuration = 0.5
let longPress =
LongPressGesture(
minimumDuration: minimumLongPressDuration
)
.sequenced(
before: DragGesture(minimumDistance: 0)
)
.updating($pressState) { value, state, _ in
switch value {
case .first:
state = .activated
case .second:
state = .confirmed
}
}
.onEnded { value in
guard case .second = value else { return }
self.viewState.toggle()
}
Image(systemName:
stateConfirmed("flashlight.on.fill", "flashlight.off.fill"))
.foregroundColor(stateConfirmed(.black, .white))
.animation(nil)
.font(.largeTitle)
.padding(32)
.background(
Circle()
.foregroundColor(
stateConfirmed(.highlightedButtonColor, .normalButtonColor)
)
.animation(nil)
)
.scaleEffect(pressState.isPressing ? 1.4 : 1)
.animation(.easeOut(duration: 0.5))
.gesture(longPress)
}
// MARK: Private
private enum PressState {
/// **Default state**.
/// The button is ready to be activiated.
case reset
/// The button with enough duration long pressing.
case activated
/// The button has recently switched on/off.
case confirmed
// MARK: Internal
var isPressing: Bool {
switch self {
case .activated:
return true
case .confirmed, .reset:
return false
}
}
var isConfirmed: Bool {
switch self {
case .activated, .reset:
return false
case .confirmed:
return true
}
}
}
@GestureState private var pressState = PressState.reset
@State var viewState: Bool = false
/// Controlled by both `viewState` and `pressState.isConfirmed` .
/// - Important: For `FlashlightButtonView` **ONLY**.
/// - Parameters:
/// - activedItem: Actived item.
/// - inactivedItem: Inactived item.
/// - Returns: `Any`.
private func stateConfirmed<T>(_ activedItem: T, _ inactivedItem: T) -> T {
return viewState ?
pressState.isConfirmed ?
activedItem : inactivedItem :
pressState.isConfirmed ?
inactivedItem : activedItem
}
}
// MARK: - FlashlightButtonView_Previews
struct FlashlightButtonView_Previews: PreviewProvider {
static var previews: some View {
FlashlightButtonView()
}
}