react-native-swipeable-actions 0.1.12 → 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -0
- package/LICENSE +21 -0
- package/README.md +377 -207
- package/android/src/main/java/expo/modules/swipeable/SwipePhysics.kt +39 -0
- package/android/src/main/java/expo/modules/swipeable/SwipeableModule.kt +24 -10
- package/android/src/main/java/expo/modules/swipeable/SwipeableView.kt +804 -420
- package/build/Swipeable.d.ts +3 -8
- package/build/Swipeable.d.ts.map +1 -1
- package/build/Swipeable.js +87 -33
- package/build/Swipeable.js.map +1 -1
- package/build/Swipeable.types.d.ts +59 -9
- package/build/Swipeable.types.d.ts.map +1 -1
- package/build/SwipeableActions.d.ts +1 -1
- package/build/SwipeableActions.d.ts.map +1 -1
- package/build/SwipeableActions.js +4 -2
- package/build/SwipeableActions.js.map +1 -1
- package/build/SwipeableView.d.ts +11 -2
- package/build/SwipeableView.d.ts.map +1 -1
- package/build/SwipeableView.js.map +1 -1
- package/build/constants.d.ts +0 -1
- package/build/constants.d.ts.map +1 -1
- package/build/constants.js +0 -1
- package/build/constants.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/nativeModuleUtils.d.ts +3 -1
- package/build/nativeModuleUtils.d.ts.map +1 -1
- package/build/nativeModuleUtils.js +20 -9
- package/build/nativeModuleUtils.js.map +1 -1
- package/build/resolveSwipeableActions.d.ts +18 -0
- package/build/resolveSwipeableActions.d.ts.map +1 -0
- package/build/resolveSwipeableActions.js +26 -0
- package/build/resolveSwipeableActions.js.map +1 -0
- package/docs/migration-v1.md +191 -0
- package/docs/support.md +90 -0
- package/ios/SwipeAnimationConfig.swift +32 -0
- package/ios/SwipePhysics.swift +418 -0
- package/ios/SwipeableModule.swift +34 -12
- package/ios/SwipeableView.swift +1062 -613
- package/jestSetup.js +17 -5
- package/package.json +11 -5
- package/src/Swipeable.tsx +147 -54
- package/src/Swipeable.types.ts +50 -9
- package/src/SwipeableActions.tsx +6 -3
- package/src/SwipeableView.tsx +10 -3
- package/src/constants.ts +0 -1
- package/src/index.ts +7 -0
- package/src/nativeModuleUtils.ts +19 -8
- package/src/resolveSwipeableActions.ts +37 -0
package/ios/SwipePhysics.swift
CHANGED
|
@@ -64,3 +64,421 @@ struct SwipePhysics {
|
|
|
64
64
|
return min(abs(gestureVelocity) / normalizer, maxVelocity)
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
|
|
68
|
+
enum SwipeSide: String, CaseIterable {
|
|
69
|
+
case left
|
|
70
|
+
case right
|
|
71
|
+
|
|
72
|
+
var sign: CGFloat { self == .left ? 1 : -1 }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Gesture-owned completion survives width reconciliation, but not commands or rebinding.
|
|
76
|
+
enum SwipeAutoCloseState: Equatable {
|
|
77
|
+
case waiting(side: SwipeSide, deadline: TimeInterval)
|
|
78
|
+
case closing(side: SwipeSide)
|
|
79
|
+
|
|
80
|
+
var side: SwipeSide {
|
|
81
|
+
switch self {
|
|
82
|
+
case .waiting(let side, _), .closing(let side): return side
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func remainingDelay(at now: TimeInterval) -> TimeInterval {
|
|
87
|
+
guard case .waiting(_, let deadline) = self else { return 0 }
|
|
88
|
+
return max(0, deadline - now)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
struct SwipeReconciledState {
|
|
93
|
+
let openSide: SwipeSide?
|
|
94
|
+
let autoClose: SwipeAutoCloseState?
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// A released gesture retains its callback ownership until completion or cancellation.
|
|
98
|
+
/// Touch activation is deliberately independent: settling and auto-close have no active touch.
|
|
99
|
+
struct SwipeGestureCompletion {
|
|
100
|
+
private(set) var isPending = false
|
|
101
|
+
private var owner: [String: String]?
|
|
102
|
+
|
|
103
|
+
mutating func begin() { isPending = true; owner = nil }
|
|
104
|
+
|
|
105
|
+
mutating func begin(recyclingKey: String?, side: SwipeSide) {
|
|
106
|
+
begin()
|
|
107
|
+
var metadata = ["side": side.rawValue]
|
|
108
|
+
if let key = recyclingKey { metadata["recyclingKey"] = key }
|
|
109
|
+
owner = metadata
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
mutating func complete() { isPending = false; owner = nil }
|
|
113
|
+
|
|
114
|
+
/// Capture the abandoned owner before native work is reset. No public end is owed.
|
|
115
|
+
mutating func cancel(recyclingKey: String?, side: SwipeSide?) -> [String: String]? {
|
|
116
|
+
let capturedOwner = owner
|
|
117
|
+
guard invalidate() else { return nil }
|
|
118
|
+
if let capturedOwner = capturedOwner { return capturedOwner }
|
|
119
|
+
var metadata: [String: String] = [:]
|
|
120
|
+
if let key = recyclingKey { metadata["recyclingKey"] = key }
|
|
121
|
+
if let side = side { metadata["side"] = side.rawValue }
|
|
122
|
+
return metadata
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@discardableResult
|
|
126
|
+
mutating func invalidate(preserving: Bool = false) -> Bool {
|
|
127
|
+
guard !preserving else { return false }
|
|
128
|
+
let cancelled = isPending
|
|
129
|
+
isPending = false
|
|
130
|
+
owner = nil
|
|
131
|
+
return cancelled
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// An immutable registry binding. Its identity changes only when a view is
|
|
136
|
+
/// registered again, never when an earlier command interrupts animation work.
|
|
137
|
+
final class SwipeCommandTarget<View: AnyObject> {
|
|
138
|
+
weak var value: View?
|
|
139
|
+
let identity: ObjectIdentifier
|
|
140
|
+
|
|
141
|
+
init(_ value: View) {
|
|
142
|
+
self.value = value
|
|
143
|
+
identity = ObjectIdentifier(value)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
func perform(ifCurrent current: SwipeCommandTarget<View>?, _ command: (View) -> Void) {
|
|
147
|
+
guard self === current, let view = value else { return }
|
|
148
|
+
command(view)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Capture ownership at physical touch-down, before a pan can leave .possible.
|
|
153
|
+
/// Additional fingers cannot transfer an unfinished sequence to a new binding.
|
|
154
|
+
struct SwipeTouchBinding<Owner: AnyObject> {
|
|
155
|
+
private var touches: Set<ObjectIdentifier> = []
|
|
156
|
+
private var target: SwipeCommandTarget<Owner>?
|
|
157
|
+
|
|
158
|
+
mutating func begin<Touch: AnyObject>(_ newTouches: [Touch], target: SwipeCommandTarget<Owner>?) {
|
|
159
|
+
guard !newTouches.isEmpty else { return }
|
|
160
|
+
if touches.isEmpty { self.target = target }
|
|
161
|
+
touches.formUnion(newTouches.map { ObjectIdentifier($0) })
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
mutating func end<Touch: AnyObject>(_ endedTouches: [Touch]) {
|
|
165
|
+
touches.subtract(endedTouches.map { ObjectIdentifier($0) })
|
|
166
|
+
// Retain identity for a queued ended/cancelled action until the next down.
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
mutating func prune<Touch: AnyObject>(keeping activeTouches: [Touch]) {
|
|
170
|
+
touches.formIntersection(activeTouches.map { ObjectIdentifier($0) })
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
func isCurrent(_ current: SwipeCommandTarget<Owner>) -> Bool {
|
|
174
|
+
target === current && target?.value != nil
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// The keyed close intent of one binding. Only the view that holds a key can
|
|
179
|
+
/// persist that key's open state, so a close marks the views that hold the key,
|
|
180
|
+
/// not the key. `closedAt` is the last close or bind that makes an earlier open
|
|
181
|
+
/// intent stale. Read and write it only through SwipeOpenStateCache.
|
|
182
|
+
final class SwipeCloseMark {
|
|
183
|
+
fileprivate(set) var key: String?
|
|
184
|
+
fileprivate(set) var closedAt = 0
|
|
185
|
+
fileprivate var isTracked = false
|
|
186
|
+
|
|
187
|
+
init() {}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Only open targets are cached. Closing removes the FIFO entry as well as its value.
|
|
191
|
+
struct SwipeOpenStateCache {
|
|
192
|
+
private struct WeakMark {
|
|
193
|
+
weak var value: SwipeCloseMark?
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let capacity: Int
|
|
197
|
+
private var sides: [String: SwipeSide] = [:]
|
|
198
|
+
private var order: [String] = []
|
|
199
|
+
// Each close takes the next sequence number. An open intent records the
|
|
200
|
+
// sequence it saw, so a close issued after it wins in every binding order.
|
|
201
|
+
private var closeSequence = 0
|
|
202
|
+
// One weak entry for each live binding: memory stays per live view.
|
|
203
|
+
private var marks: [WeakMark] = []
|
|
204
|
+
|
|
205
|
+
init(capacity: Int) {
|
|
206
|
+
self.capacity = capacity
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func side(for key: String) -> SwipeSide? { sides[key] }
|
|
210
|
+
|
|
211
|
+
/// The intent an open records now. Read it on the registry queue.
|
|
212
|
+
var currentIntent: Int { closeSequence }
|
|
213
|
+
|
|
214
|
+
/// False if a close issued after the intent marked this binding.
|
|
215
|
+
func isCurrent(_ intent: Int, for mark: SwipeCloseMark) -> Bool {
|
|
216
|
+
intent >= mark.closedAt
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
var trackedMarkCount: Int { marks.count }
|
|
220
|
+
|
|
221
|
+
/// A queued close skips a binding that opened after the close command:
|
|
222
|
+
/// that open is the latest intent and wins.
|
|
223
|
+
static func closeApplies(_ close: Int, toOpenIntent openIntent: Int) -> Bool {
|
|
224
|
+
openIntent < close
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// The cached side and the intent it holds, read in one step.
|
|
228
|
+
func restore(for key: String) -> (side: SwipeSide?, intent: Int) {
|
|
229
|
+
(sides[key], closeSequence)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/// Moves a binding to its new key and reads that key's restore in one step.
|
|
233
|
+
/// The binding no longer holds the old key, so the old key's mark ends. An intent
|
|
234
|
+
/// stamped before the bind belongs to an earlier binding and cannot persist the new key.
|
|
235
|
+
mutating func bind(_ mark: SwipeCloseMark, to key: String?) -> (side: SwipeSide?, intent: Int)? {
|
|
236
|
+
if !mark.isTracked {
|
|
237
|
+
mark.isTracked = true
|
|
238
|
+
marks.removeAll { $0.value == nil }
|
|
239
|
+
marks.append(WeakMark(value: mark))
|
|
240
|
+
}
|
|
241
|
+
if mark.key != key {
|
|
242
|
+
mark.key = key
|
|
243
|
+
mark.closedAt = closeSequence
|
|
244
|
+
}
|
|
245
|
+
return key.map { restore(for: $0) }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private mutating func markLiveBindings(where holds: (SwipeCloseMark) -> Bool) {
|
|
249
|
+
marks.removeAll { $0.value == nil }
|
|
250
|
+
for entry in marks {
|
|
251
|
+
if let mark = entry.value, holds(mark) { mark.closedAt = closeSequence }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Call while holding the registry queue: capture and the close must be atomic.
|
|
256
|
+
// The close marks every binding that holds the key, the registered one and
|
|
257
|
+
// any older one: no binding can restore or persist an earlier open.
|
|
258
|
+
mutating func captureCloseTarget<View: AnyObject>(
|
|
259
|
+
for key: String, current: SwipeCommandTarget<View>?
|
|
260
|
+
) -> SwipeCommandTarget<View>? {
|
|
261
|
+
closeSequence += 1
|
|
262
|
+
markLiveBindings { $0.key == key }
|
|
263
|
+
set(nil, for: key)
|
|
264
|
+
return current?.value != nil ? current : nil
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Like captureCloseTarget for every key, bound or not, mounted or not.
|
|
268
|
+
mutating func closeAll() {
|
|
269
|
+
closeSequence += 1
|
|
270
|
+
markLiveBindings { _ in true }
|
|
271
|
+
sides.removeAll()
|
|
272
|
+
order.removeAll()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// An open that a later close made stale persists closed. A binding that
|
|
276
|
+
/// no longer holds the key cannot persist it open.
|
|
277
|
+
mutating func persist(_ side: SwipeSide?, for key: String, from mark: SwipeCloseMark, intent: Int) {
|
|
278
|
+
set(mark.key == key && isCurrent(intent, for: mark) ? side : nil, for: key)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
mutating func set(_ side: SwipeSide?, for key: String) {
|
|
282
|
+
guard let side = side else {
|
|
283
|
+
sides.removeValue(forKey: key)
|
|
284
|
+
order.removeAll { $0 == key }
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
if sides[key] == nil { order.append(key) }
|
|
288
|
+
sides[key] = side
|
|
289
|
+
while order.count > max(0, capacity) {
|
|
290
|
+
sides.removeValue(forKey: order.removeFirst())
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// Only Fabric's mount/unmount hooks participate in role assignment.
|
|
296
|
+
struct SwipeReactChildren<Child: AnyObject> {
|
|
297
|
+
private(set) var views: [Child] = []
|
|
298
|
+
|
|
299
|
+
mutating func mount(_ child: Child, at index: Int) {
|
|
300
|
+
views.removeAll { $0 === child }
|
|
301
|
+
views.insert(child, at: min(max(index, 0), views.count))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
mutating func unmount(_ child: Child, at index: Int) {
|
|
305
|
+
if views.indices.contains(index), views[index] === child {
|
|
306
|
+
views.remove(at: index)
|
|
307
|
+
} else {
|
|
308
|
+
views.removeAll { $0 === child }
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
struct SwipeChildRoles: Equatable {
|
|
314
|
+
let left: Int?
|
|
315
|
+
let right: Int?
|
|
316
|
+
let content: Int
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
protocol SwipeChildVisibility: AnyObject {
|
|
320
|
+
var isHidden: Bool { get set }
|
|
321
|
+
var isUserInteractionEnabled: Bool { get set }
|
|
322
|
+
var accessibilityElementsHidden: Bool { get set }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// Return native-owned rail flags before Fabric reuses a shell as arbitrary content.
|
|
326
|
+
/// Keep the caller's original values; unowned content must not be forcibly unhidden.
|
|
327
|
+
final class SwipeRailVisibility {
|
|
328
|
+
private struct State {
|
|
329
|
+
weak var view: SwipeChildVisibility?
|
|
330
|
+
let isHidden: Bool
|
|
331
|
+
let isUserInteractionEnabled: Bool
|
|
332
|
+
let accessibilityElementsHidden: Bool
|
|
333
|
+
|
|
334
|
+
init(_ view: SwipeChildVisibility) {
|
|
335
|
+
self.view = view
|
|
336
|
+
isHidden = view.isHidden
|
|
337
|
+
isUserInteractionEnabled = view.isUserInteractionEnabled
|
|
338
|
+
accessibilityElementsHidden = view.accessibilityElementsHidden
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func restore() {
|
|
342
|
+
view?.isHidden = isHidden
|
|
343
|
+
view?.isUserInteractionEnabled = isUserInteractionEnabled
|
|
344
|
+
view?.accessibilityElementsHidden = accessibilityElementsHidden
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
private var states: [ObjectIdentifier: State] = [:]
|
|
349
|
+
|
|
350
|
+
func setRevealed(_ revealed: Bool, for view: SwipeChildVisibility) {
|
|
351
|
+
let identity = ObjectIdentifier(view)
|
|
352
|
+
if states[identity]?.view !== view { states[identity] = State(view) }
|
|
353
|
+
view.isHidden = !revealed
|
|
354
|
+
view.isUserInteractionEnabled = revealed
|
|
355
|
+
view.accessibilityElementsHidden = !revealed
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
func release(_ view: SwipeChildVisibility) {
|
|
359
|
+
guard let state = states.removeValue(forKey: ObjectIdentifier(view)),
|
|
360
|
+
state.view === view else { return }
|
|
361
|
+
state.restore()
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func releaseAll() {
|
|
365
|
+
for state in states.values { state.restore() }
|
|
366
|
+
states.removeAll()
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// Threshold state is independent of the feedback preference and the animation clock.
|
|
371
|
+
struct SwipeFeedbackState {
|
|
372
|
+
private(set) var isActive = false
|
|
373
|
+
private(set) var hasCrossedThreshold = false
|
|
374
|
+
private var wasAboveThreshold = false
|
|
375
|
+
|
|
376
|
+
mutating func activate(progress: CGFloat, threshold: CGFloat) {
|
|
377
|
+
activate(isOpening: progress > threshold)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
mutating func activate(isOpening: Bool) {
|
|
381
|
+
isActive = true
|
|
382
|
+
hasCrossedThreshold = false
|
|
383
|
+
wasAboveThreshold = isOpening
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
mutating func update(progress: CGFloat, threshold: CGFloat) -> Bool {
|
|
387
|
+
update(isOpening: progress > threshold)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
mutating func update(isOpening: Bool) -> Bool {
|
|
391
|
+
guard isActive else { return false }
|
|
392
|
+
let above = isOpening
|
|
393
|
+
let firstCrossing = above && !wasAboveThreshold && !hasCrossedThreshold
|
|
394
|
+
wasAboveThreshold = above
|
|
395
|
+
if firstCrossing { hasCrossedThreshold = true }
|
|
396
|
+
return firstCrossing
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
mutating func reset() {
|
|
400
|
+
isActive = false
|
|
401
|
+
hasCrossedThreshold = false
|
|
402
|
+
wasAboveThreshold = false
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
extension SwipePhysics {
|
|
407
|
+
static func reconcileTwoSidedState(
|
|
408
|
+
openSide: SwipeSide?, autoCloseState: SwipeAutoCloseState?,
|
|
409
|
+
leftWidth: CGFloat, rightWidth: CGFloat,
|
|
410
|
+
wasGestureEnabled: Bool, gestureEnabled: Bool, autoClose: Bool
|
|
411
|
+
) -> SwipeReconciledState {
|
|
412
|
+
// Disabling is a transition. A subsequent imperative open remains valid while disabled.
|
|
413
|
+
guard !wasGestureEnabled || gestureEnabled else {
|
|
414
|
+
return SwipeReconciledState(openSide: nil, autoClose: nil)
|
|
415
|
+
}
|
|
416
|
+
func enabled(_ side: SwipeSide) -> Bool {
|
|
417
|
+
validWidth(side == .left ? leftWidth : rightWidth) > 0
|
|
418
|
+
}
|
|
419
|
+
let restored = openSide.flatMap { enabled($0) ? $0 : nil }
|
|
420
|
+
let pending = autoCloseState.flatMap { state -> SwipeAutoCloseState? in
|
|
421
|
+
guard autoClose, enabled(state.side) else { return nil }
|
|
422
|
+
switch state {
|
|
423
|
+
case .waiting: return restored == state.side ? state : nil
|
|
424
|
+
case .closing: return restored == nil ? state : nil
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return SwipeReconciledState(openSide: restored, autoClose: pending)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
static func shouldRestoreAfterDetach(hasPendingInteraction: Bool) -> Bool {
|
|
431
|
+
!hasPendingInteraction
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
static func shouldCloseForCloseAll(
|
|
435
|
+
isOpen: Bool, isAnimating: Bool, hasPendingInteraction: Bool
|
|
436
|
+
) -> Bool {
|
|
437
|
+
// A closing animator can own a close-then-open continuation while logically closed.
|
|
438
|
+
isOpen || isAnimating || hasPendingInteraction
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
static func validWidth(_ width: CGFloat) -> CGFloat {
|
|
442
|
+
width.isFinite && width > 0 ? width : 0
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
static func childRoles(count: Int) -> SwipeChildRoles? {
|
|
446
|
+
guard count == 3 else { return nil }
|
|
447
|
+
return SwipeChildRoles(left: 0, right: 1, content: 2)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
static func latchedSide(
|
|
451
|
+
openSide: SwipeSide?, translation: CGFloat, leftWidth: CGFloat, rightWidth: CGFloat
|
|
452
|
+
) -> SwipeSide? {
|
|
453
|
+
let candidate: SwipeSide?
|
|
454
|
+
if let openSide = openSide {
|
|
455
|
+
candidate = openSide
|
|
456
|
+
} else {
|
|
457
|
+
candidate = translation > 0 ? .left : (translation < 0 ? .right : nil)
|
|
458
|
+
}
|
|
459
|
+
guard let side = candidate,
|
|
460
|
+
validWidth(side == .left ? leftWidth : rightWidth) > 0 else { return nil }
|
|
461
|
+
return side
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
static func constrainToSide(translation: CGFloat, actionsWidth: CGFloat, side: SwipeSide) -> CGFloat {
|
|
465
|
+
guard validWidth(actionsWidth) > 0 else { return 0 }
|
|
466
|
+
return applyRubberBand(
|
|
467
|
+
translation: side == .left ? max(0, translation) : min(0, translation),
|
|
468
|
+
actionsWidth: actionsWidth, isLeading: side == .left
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
static func shouldOpenOnSide(
|
|
473
|
+
translation: CGFloat, velocity: CGFloat, friction: CGFloat,
|
|
474
|
+
actionsWidth: CGFloat, threshold: CGFloat, side: SwipeSide, isCurrentlyOpen: Bool
|
|
475
|
+
) -> Bool {
|
|
476
|
+
// Translation is relative gesture displacement. The legacy snap helper accounts
|
|
477
|
+
// for the starting-open state; adding the row's start offset here counts it twice.
|
|
478
|
+
shouldSnapToOpen(
|
|
479
|
+
projectedTranslation: projectFinalPosition(translation: translation, velocity: velocity, friction: friction),
|
|
480
|
+
actionsWidth: actionsWidth, threshold: threshold,
|
|
481
|
+
isLeading: side == .left, isCurrentlyOpen: isCurrentlyOpen
|
|
482
|
+
)
|
|
483
|
+
}
|
|
484
|
+
}
|
|
@@ -25,16 +25,38 @@ public class SwipeableModule: Module {
|
|
|
25
25
|
SwipeableView.getOpenState(for: key)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
Function("openByKeyOnSide") { (key: String, side: String) in
|
|
29
|
+
SwipeableView.openByKeyOnSide(key, side: side)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Function("getOpenSideByKey") { (key: String) -> String? in
|
|
33
|
+
SwipeableView.getOpenSide(for: key)
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
View(SwipeableView.self) {
|
|
29
|
-
|
|
30
|
-
Prop("
|
|
31
|
-
view.
|
|
37
|
+
|
|
38
|
+
Prop("leftActionsWidth") { (view: SwipeableView, width: CGFloat) in
|
|
39
|
+
view.leftActionsWidth = width
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Prop("rightActionsWidth") { (view: SwipeableView, width: CGFloat) in
|
|
43
|
+
view.rightActionsWidth = width
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Prop("appearance") { (view: SwipeableView, value: String) in
|
|
47
|
+
view.appearance = value
|
|
32
48
|
}
|
|
33
49
|
|
|
34
|
-
Prop("
|
|
35
|
-
view.
|
|
50
|
+
Prop("hapticFeedback") { (view: SwipeableView, value: Bool?) in
|
|
51
|
+
view.hapticFeedback = value
|
|
36
52
|
}
|
|
37
53
|
|
|
54
|
+
OnViewDidUpdateProps { (view: SwipeableView) in
|
|
55
|
+
view.commitProps()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Props
|
|
59
|
+
|
|
38
60
|
Prop("friction") { (view: SwipeableView, friction: CGFloat) in
|
|
39
61
|
view.friction = friction
|
|
40
62
|
}
|
|
@@ -64,24 +86,24 @@ public class SwipeableModule: Module {
|
|
|
64
86
|
}
|
|
65
87
|
|
|
66
88
|
// Events
|
|
67
|
-
Events("onSwipeProgress", "onSwipeStart", "onSwipeStateChange", "onSwipeEnd")
|
|
89
|
+
Events("onSwipeProgress", "onSwipeStart", "onSwipeStateChange", "onSwipeEnd", "onActionsNeeded", "onGestureCancelled")
|
|
68
90
|
|
|
69
91
|
// Imperative methods
|
|
70
92
|
AsyncFunction("close") { (view: SwipeableView, animated: Bool?) in
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
view.enqueueCommand {
|
|
94
|
+
$0.close(animated: animated ?? true)
|
|
73
95
|
}
|
|
74
96
|
}
|
|
75
97
|
|
|
76
98
|
AsyncFunction("open") { (view: SwipeableView) in
|
|
77
|
-
|
|
78
|
-
|
|
99
|
+
view.enqueueCommand {
|
|
100
|
+
$0.open()
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
|
|
82
104
|
AsyncFunction("cancel") { (view: SwipeableView) in
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
view.enqueueCommand {
|
|
106
|
+
$0.cancelGesture()
|
|
85
107
|
}
|
|
86
108
|
}
|
|
87
109
|
|