forked from SwipeCellKit/SwipeCellKit
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSwipeExpansionStyle.swift
233 lines (182 loc) · 10.7 KB
/
SwipeExpansionStyle.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
221
222
223
224
225
226
227
228
229
230
231
232
233
//
// SwipeExpansionStyle.swift
//
// Created by Jeremy Koch
// Copyright © 2017 Jeremy Koch. All rights reserved.
//
import UIKit
/// Describes the expansion style. Expansion is the behavior when the cell is swiped past a defined threshold.
public struct SwipeExpansionStyle {
/// The default action performs a selection-type behavior. The cell bounces back to its unopened state upon selection and the row remains in the table view.
public static var selection: SwipeExpansionStyle { return SwipeExpansionStyle(target: .percentage(0.5),
elasticOverscroll: true,
completionAnimation: .bounce) }
/// The default action performs a destructive behavior. The cell is removed from the table view in an animated fashion.
public static var destructive: SwipeExpansionStyle { return .destructive(automaticallyDelete: true, timing: .with) }
/// The default action performs a destructive behavior after the fill animation completes. The cell is removed from the table view in an animated fashion.
public static var destructiveAfterFill: SwipeExpansionStyle { return .destructive(automaticallyDelete: true, timing: .after) }
/// The default action performs a fill behavior.
///
/// - note: The action handle must call `SwipeAction.fulfill(style:)` to resolve the fill expansion.
public static var fill: SwipeExpansionStyle { return SwipeExpansionStyle(target: .edgeInset(30),
additionalTriggers: [.overscroll(30)],
completionAnimation: .fill(.manual(timing: .after))) }
/**
Returns a `SwipeExpansionStyle` instance for the default action which peforms destructive behavior with the specified options.
- parameter automaticallyDelete: Specifies if row/item deletion should be peformed automatically. If `false`, you must call `SwipeAction.fulfill(with style:)` at some point while/after your action handler is invoked to trigger deletion.
- parameter timing: The timing which specifies when the action handler will be invoked with respect to the fill animation.
- returns: The new `SwipeExpansionStyle` instance.
*/
public static func destructive(automaticallyDelete: Bool, timing: FillOptions.HandlerInvocationTiming = .with) -> SwipeExpansionStyle {
return SwipeExpansionStyle(target: .edgeInset(30),
additionalTriggers: [.touchThreshold(0.8)],
completionAnimation: .fill(automaticallyDelete ? .automatic(.delete, timing: timing) : .manual(timing: timing)))
}
/// The relative target expansion threshold. Expansion will occur at the specified value.
public let target: Target
/// Additional triggers to useful for determining if expansion should occur.
public let additionalTriggers: [Trigger]
/// Specifies if buttons should expand to fully fill overscroll, or expand at a percentage relative to the overscroll.
public let elasticOverscroll: Bool
/// Specifies the expansion animation completion style.
public let completionAnimation: CompletionAnimation
/// Specifies the minimum amount of overscroll required if the configured target is less than the fully exposed action view.
public var minimumTargetOverscroll: CGFloat = 20
/// The amount of elasticity applied when dragging past the expansion target.
///
/// - note: Default value is 0.2. Valid range is from 0.0 for no movement past the expansion target, to 1.0 for unrestricted movement with dragging.
public var targetOverscrollElasticity: CGFloat = 0.2
/**
Contructs a new `SwipeExpansionStyle` instance.
- parameter target: The relative target expansion threshold. Expansion will occur at the specified value.
- parameter additionalTriggers: Additional triggers to useful for determining if expansion should occur.
- parameter elasticOverscroll: Specifies if buttons should expand to fully fill overscroll, or expand at a percentage relative to the overscroll.
- parameter completionAnimation: Specifies the expansion animation completion style.
- returns: The new `SwipeExpansionStyle` instance.
*/
public init(target: Target, additionalTriggers: [Trigger] = [], elasticOverscroll: Bool = false, completionAnimation: CompletionAnimation = .bounce) {
self.target = target
self.additionalTriggers = additionalTriggers
self.elasticOverscroll = elasticOverscroll
self.completionAnimation = completionAnimation
}
func shouldExpand(view: Swipeable, gesture: UIPanGestureRecognizer, in superview: UIView) -> Bool {
guard let actionsView = view.actionsView else { return false }
guard abs(view.frame.minX) >= actionsView.preferredWidth else { return false }
if abs(view.frame.minX) >= target.offset(for: view, in: superview, minimumOverscroll: minimumTargetOverscroll) {
return true
}
for trigger in additionalTriggers {
if trigger.isTriggered(view: view, gesture: gesture, in: superview) {
return true
}
}
return false
}
func targetOffset(for view: Swipeable, in superview: UIView) -> CGFloat {
return target.offset(for: view, in: superview, minimumOverscroll: minimumTargetOverscroll)
}
}
extension SwipeExpansionStyle {
/// Describes the relative target expansion threshold. Expansion will occur at the specified value.
public enum Target {
/// The target is specified by a percentage.
case percentage(CGFloat)
/// The target is specified by a edge inset.
case edgeInset(CGFloat)
func offset(for view: Swipeable, in superview: UIView, minimumOverscroll: CGFloat) -> CGFloat {
guard let actionsView = view.actionsView else { return .greatestFiniteMagnitude }
let offset: CGFloat = {
switch self {
case .percentage(let value):
return superview.bounds.width * value
case .edgeInset(let value):
return superview.bounds.width - value
}
}()
return max(actionsView.preferredWidth + minimumOverscroll, offset)
}
}
/// Describes additional triggers to useful for determining if expansion should occur.
public enum Trigger {
/// The trigger is specified by a touch occuring past the supplied percentage in the superview.
case touchThreshold(CGFloat)
/// The trigger is specified by the distance in points past the fully exposed action view.
case overscroll(CGFloat)
func isTriggered(view: Swipeable, gesture: UIPanGestureRecognizer, in superview: UIView) -> Bool {
guard let actionsView = view.actionsView else { return false }
switch self {
case .touchThreshold(let value):
let location = gesture.location(in: superview).x
let locationRatio = (actionsView.orientation == .left ? location : superview.bounds.width - location) / superview.bounds.width
return locationRatio > value
case .overscroll(let value):
return abs(view.frame.minX) > actionsView.preferredWidth + value
}
}
}
/// Describes the expansion animation completion style.
public enum CompletionAnimation {
/// The expansion will completely fill the item.
case fill(FillOptions)
/// The expansion will bounce back from the trigger point and hide the action view, resetting the item.
case bounce
}
/// Specifies the options for the fill completion animation.
public struct FillOptions {
/// Describes when the action handler will be invoked with respect to the fill animation.
public enum HandlerInvocationTiming {
/// The action handler is invoked with the fill animation.
case with
/// The action handler is invoked after the fill animation completes.
case after
}
/**
Returns a `FillOptions` instance with automatic fulfillemnt.
- parameter style: The fulfillment style describing how expansion should be resolved once the action has been fulfilled.
- parameter timing: The timing which specifies when the action handler will be invoked with respect to the fill animation.
- returns: The new `FillOptions` instance.
*/
public static func automatic(_ style: ExpansionFulfillmentStyle, timing: HandlerInvocationTiming) -> FillOptions {
return FillOptions(autoFulFillmentStyle: style, timing: timing)
}
/**
Returns a `FillOptions` instance with manual fulfillemnt.
- parameter timing: The timing which specifies when the action handler will be invoked with respect to the fill animation.
- returns: The new `FillOptions` instance.
*/
public static func manual(timing: HandlerInvocationTiming) -> FillOptions {
return FillOptions(autoFulFillmentStyle: nil, timing: timing)
}
/// The fulfillment style describing how expansion should be resolved once the action has been fulfilled.
public let autoFulFillmentStyle: ExpansionFulfillmentStyle?
/// The timing which specifies when the action handler will be invoked with respect to the fill animation.
public let timing: HandlerInvocationTiming
}
}
extension SwipeExpansionStyle.Target: Equatable {
/// :nodoc:
public static func ==(lhs: SwipeExpansionStyle.Target, rhs: SwipeExpansionStyle.Target) -> Bool {
switch (lhs, rhs) {
case (.percentage(let lhs), .percentage(let rhs)):
return lhs == rhs
case (.edgeInset(let lhs), .edgeInset(let rhs)):
return lhs == rhs
default:
return false
}
}
}
extension SwipeExpansionStyle.CompletionAnimation: Equatable {
/// :nodoc:
public static func ==(lhs: SwipeExpansionStyle.CompletionAnimation, rhs: SwipeExpansionStyle.CompletionAnimation) -> Bool {
switch (lhs, rhs) {
case (.fill, .fill):
return true
case (.bounce, .bounce):
return true
default:
return false
}
}
}