react-native-swipeable-actions 0.1.11 → 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.
Files changed (50) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/LICENSE +21 -0
  3. package/README.md +377 -207
  4. package/android/src/main/java/expo/modules/swipeable/SwipePhysics.kt +39 -0
  5. package/android/src/main/java/expo/modules/swipeable/SwipeableModule.kt +24 -10
  6. package/android/src/main/java/expo/modules/swipeable/SwipeableView.kt +804 -420
  7. package/build/Swipeable.d.ts +3 -8
  8. package/build/Swipeable.d.ts.map +1 -1
  9. package/build/Swipeable.js +87 -33
  10. package/build/Swipeable.js.map +1 -1
  11. package/build/Swipeable.types.d.ts +59 -9
  12. package/build/Swipeable.types.d.ts.map +1 -1
  13. package/build/SwipeableActions.d.ts +1 -1
  14. package/build/SwipeableActions.d.ts.map +1 -1
  15. package/build/SwipeableActions.js +4 -2
  16. package/build/SwipeableActions.js.map +1 -1
  17. package/build/SwipeableView.d.ts +11 -2
  18. package/build/SwipeableView.d.ts.map +1 -1
  19. package/build/SwipeableView.js.map +1 -1
  20. package/build/constants.d.ts +0 -1
  21. package/build/constants.d.ts.map +1 -1
  22. package/build/constants.js +0 -1
  23. package/build/constants.js.map +1 -1
  24. package/build/index.d.ts +1 -1
  25. package/build/index.d.ts.map +1 -1
  26. package/build/index.js.map +1 -1
  27. package/build/nativeModuleUtils.d.ts +3 -1
  28. package/build/nativeModuleUtils.d.ts.map +1 -1
  29. package/build/nativeModuleUtils.js +20 -9
  30. package/build/nativeModuleUtils.js.map +1 -1
  31. package/build/resolveSwipeableActions.d.ts +18 -0
  32. package/build/resolveSwipeableActions.d.ts.map +1 -0
  33. package/build/resolveSwipeableActions.js +26 -0
  34. package/build/resolveSwipeableActions.js.map +1 -0
  35. package/docs/migration-v1.md +191 -0
  36. package/docs/support.md +90 -0
  37. package/ios/SwipeAnimationConfig.swift +32 -0
  38. package/ios/SwipePhysics.swift +418 -0
  39. package/ios/SwipeableModule.swift +34 -12
  40. package/ios/SwipeableView.swift +1061 -600
  41. package/jestSetup.js +17 -5
  42. package/package.json +11 -5
  43. package/src/Swipeable.tsx +147 -54
  44. package/src/Swipeable.types.ts +50 -9
  45. package/src/SwipeableActions.tsx +6 -3
  46. package/src/SwipeableView.tsx +10 -3
  47. package/src/constants.ts +0 -1
  48. package/src/index.ts +7 -0
  49. package/src/nativeModuleUtils.ts +19 -8
  50. package/src/resolveSwipeableActions.ts +37 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveSwipeableActions.d.ts","sourceRoot":"","sources":["../src/resolveSwipeableActions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAGtC,UAAU,YAAY;IACpB,WAAW,CAAC,EAAE,SAAS,CAAA;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,YAAY,CAAC,EAAE,SAAS,CAAA;IACxB,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd;AAMD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,YAAY;;;;EAiB1D"}
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveSwipeableActions = resolveSwipeableActions;
4
+ const isContent = (node) => node != null && typeof node !== 'boolean';
5
+ const isWidth = (width) => width !== undefined && Number.isFinite(width) && width > 0;
6
+ function resolveSwipeableActions(input) {
7
+ const warnings = [];
8
+ const resolve = (side, node, width) => {
9
+ if (node !== undefined) {
10
+ if (!isContent(node))
11
+ return { node: null, width: 0 };
12
+ if (isWidth(width))
13
+ return { node, width };
14
+ warnings.push(side + 'Actions requires a finite, positive ' + side + 'ActionsWidth.');
15
+ return { node: null, width: 0 };
16
+ }
17
+ if (width !== undefined) {
18
+ warnings.push(side + 'ActionsWidth is ignored without explicit ' + side + 'Actions.');
19
+ }
20
+ return { node: null, width: 0 };
21
+ };
22
+ const left = resolve('left', input.leftActions, input.leftActionsWidth);
23
+ const right = resolve('right', input.rightActions, input.rightActionsWidth);
24
+ return { left, right, warnings };
25
+ }
26
+ //# sourceMappingURL=resolveSwipeableActions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolveSwipeableActions.js","sourceRoot":"","sources":["../src/resolveSwipeableActions.ts"],"names":[],"mappings":";;AAmBA,0DAiBC;AArBD,MAAM,SAAS,GAAG,CAAC,IAAe,EAAE,EAAE,CAAC,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,SAAS,CAAA;AAChF,MAAM,OAAO,GAAG,CAAC,KAAyB,EAAmB,EAAE,CAC7D,KAAK,KAAK,SAAS,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAA;AAE5D,SAAgB,uBAAuB,CAAC,KAAmB;IACzD,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,MAAM,OAAO,GAAG,CAAC,IAAe,EAAE,IAAe,EAAE,KAAc,EAAmB,EAAE;QACpF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;YACrD,IAAI,OAAO,CAAC,KAAK,CAAC;gBAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAA;YAC1C,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,sCAAsC,GAAG,IAAI,GAAG,eAAe,CAAC,CAAA;YACrF,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;QACjC,CAAC;QACD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,QAAQ,CAAC,IAAI,CAAC,IAAI,GAAG,2CAA2C,GAAG,IAAI,GAAG,UAAU,CAAC,CAAA;QACvF,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,CAAA;IACjC,CAAC,CAAA;IACD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,gBAAgB,CAAC,CAAA;IACvE,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,YAAY,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAA;IAC3E,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAA;AAClC,CAAC"}
@@ -0,0 +1,191 @@
1
+ # Migrating to v1
2
+
3
+ The component name stays `Swipeable`. All rows now use explicit physical-side
4
+ content/width pairs and one native engine. Rebuild the native app after upgrading.
5
+ Do not mix v1 JavaScript with an older native binary.
6
+
7
+ ## Before you install
8
+
9
+ - v1 requires the New Architecture (Fabric) on iOS and Android. The legacy
10
+ architecture (Paper) is not supported. If your app still uses it, stay on 0.x.
11
+ - v1 requires Expo SDK 54 or newer, React Native 0.81 or newer and React 19.1 or
12
+ newer. Upgrade the app first, then install v1.
13
+ - Install it with `npx expo install react-native-swipeable-actions@next` while
14
+ 1.0 is a release candidate, then rebuild the iOS and Android apps.
15
+
16
+ ## At a glance
17
+
18
+ | Kind | 0.x | 1.0 |
19
+ | --- | --- | --- |
20
+ | Removed | `actions`, `actionsWidth` | `rightActions`/`rightActionsWidth` or `leftActions`/`leftActionsWidth` |
21
+ | Removed | `actionsPosition` | Choose the side by which pair you pass. Use `open(side)` for commands. |
22
+ | Removed | `SWIPE_DEFAULTS.ACTIONS_POSITION` | No replacement. There is no default side. |
23
+ | Removed | Raw `SwipeableView` `actionsWidth`, `actionsPosition` | `leftActionsWidth`/`rightActionsWidth`. The raw view is low-level; prefer `Swipeable`. |
24
+ | Removed | Fallback between old and new props | Resolve content in your code, then pass explicit pairs. |
25
+ | Changed shape | `interface SwipeableProps` | `type SwipeableProps`, a union of valid side pairs. Extend it with `&`, not `extends`. |
26
+ | Unchanged | `open`, `close`, `cancel`, `closeAll`, static keyed methods | Same names. `open` takes an optional side. |
27
+ | Unchanged | `onSwipeStart`, `onSwipeStateChange`, `onSwipeEnd`, `onProgress` | Same arguments. Some timing changed, see below. |
28
+ | New | - | `onLeftSwipeEnd`, `onRightSwipeEnd`, `appearance`, `hapticFeedback` |
29
+ | New | - | Type aliases `TwoSidedSwipeableProps` and `SwipeableComponentProps` (both equal `SwipeableProps`), `SwipeSide`, `SwipeAppearance` |
30
+
31
+ ## Replace the previous single-sided props
32
+
33
+ Before, right/default actions:
34
+
35
+ ```tsx
36
+ <Swipeable actions={<DeleteAction />} actionsWidth={80}>
37
+ <Row />
38
+ </Swipeable>
39
+ ```
40
+
41
+ After:
42
+
43
+ ```tsx
44
+ <Swipeable rightActions={<DeleteAction />} rightActionsWidth={80}>
45
+ <Row />
46
+ </Swipeable>
47
+ ```
48
+
49
+ Before, left actions:
50
+
51
+ ```tsx
52
+ <Swipeable actions={<ReplyAction />} actionsWidth={60} actionsPosition="left">
53
+ <Message />
54
+ </Swipeable>
55
+ ```
56
+
57
+ After:
58
+
59
+ ```tsx
60
+ <Swipeable leftActions={<ReplyAction />} leftActionsWidth={60}>
61
+ <Message />
62
+ </Swipeable>
63
+ ```
64
+
65
+ For both sides, supply both complete pairs. Select alternate/default content in
66
+ the caller before passing it; there is no legacy fallback or precedence resolution.
67
+
68
+ | Previous input | v1 replacement |
69
+ | --------------------------------------------- | ------------------------------------------------------------- |
70
+ | `actions` and `actionsWidth` on right/default | `rightActions` and `rightActionsWidth` |
71
+ | `actions` and `actionsWidth` on left | `leftActions` and `leftActionsWidth` |
72
+ | `actionsPosition` | Select explicit pairs; use `open(side)` for command selection |
73
+ | Mixed old and explicit props | Caller resolves content, then passes explicit pairs |
74
+
75
+ An enabled side needs a finite positive width. Disabled/omitted content has no
76
+ width and cannot borrow another edge's width. `null`, `false`, and `undefined`
77
+ disable content; numeric `0` is still valid React content. Runtime invalid-width
78
+ handling disables the edge and warns in development rather than enabling fallback.
79
+ A fully disabled configuration can use `leftActions={null} rightActions={false}`.
80
+
81
+ ### Full-width swipe with no visible actions
82
+
83
+ Some 0.x screens used `actions={null}` with a full-width `actionsWidth` to slide
84
+ the whole screen, for example to swipe back. Before:
85
+
86
+ ```tsx
87
+ <Swipeable actions={null} actionsWidth={width} actionsPosition="left">
88
+ <Screen />
89
+ </Swipeable>
90
+ ```
91
+
92
+ After:
93
+
94
+ ```tsx
95
+ <Swipeable leftActions={<View />} leftActionsWidth={width}>
96
+ <Screen />
97
+ </Swipeable>
98
+ ```
99
+
100
+ In v1, `null` disables the side, so the swipe does not start. An empty `<View />`
101
+ keeps the side enabled and shows nothing behind the content. See
102
+ [Swipe to go back](../README.md#swipe-to-go-back) for the full recipe.
103
+
104
+ ## Types: removed inputs versus retained aliases
105
+
106
+ `SwipeableProps` is the canonical explicit-side shape.
107
+ `TwoSidedSwipeableProps` and `SwipeableComponentProps` remain aliases of that same
108
+ shape. They are not an old/new compatibility union and do not accept removed props.
109
+
110
+ The previous extendable interface is now a union-based side configuration.
111
+ Use an intersection for wrapper-specific fields, not an interface extending that
112
+ union. Keep each content/width pair together instead of independently widening
113
+ its fields to optional values.
114
+
115
+ ```tsx
116
+ import { forwardRef } from "react";
117
+ import {
118
+ Swipeable,
119
+ type SwipeableMethods,
120
+ type SwipeableProps,
121
+ } from "react-native-swipeable-actions";
122
+
123
+ type MessageSwipeProps = SwipeableProps & { itemKey: string };
124
+
125
+ export const MessageSwipe = forwardRef<SwipeableMethods, MessageSwipeProps>(
126
+ function MessageSwipe({ itemKey, ...props }, ref) {
127
+ return <Swipeable {...props} ref={ref} recyclingKey={itemKey} />;
128
+ },
129
+ );
130
+ ```
131
+
132
+ The ref/static method names and callback signatures remain. `SwipeSide` is
133
+ `'left' | 'right'`; `SwipeAppearance` is `'default' | 'ios26'`.
134
+ Native child layout and side/key event routing are internal implementation
135
+ details, not new arguments to the component's public callbacks.
136
+
137
+ ## Commands and default selection
138
+
139
+ `open()` and `Swipeable.open(key)` choose enabled right first, otherwise enabled
140
+ left. With neither enabled they do nothing. Use `open('left')` when both sides are
141
+ enabled and left is intended. An explicit disabled side never opens the opposite
142
+ edge. Opening the opposite enabled side closes the current edge before opening it.
143
+
144
+ `close(animated?)`, `cancel()`, and the static keyed operations remain.
145
+ Ref commands still need a usable recycling key. Numeric keys become strings for
146
+ the static API. Stable keys identify items, not their current list position or language.
147
+
148
+ ## Intentional behavior changes
149
+
150
+ Migrated single-side rows deliberately adopt the canonical engine's behavior.
151
+ The goal is not to reproduce old implementation quirks.
152
+
153
+ - **Disabled directions:** dragging toward a disabled edge stays closed without
154
+ starting a swipe lifecycle. The old wrong-direction rubber-band behavior is gone.
155
+ - **State callbacks:** threshold/intent transitions follow the enabled, latched
156
+ edge. Code must not depend on the old single-side suppression or incidental
157
+ callback cadence. Public callback argument lists are unchanged.
158
+ - **Cancellation:** cancels pending/active interaction and cached open intent
159
+ without completing the action. Continuing the same cancelled touch must not
160
+ restart it; a fresh gesture can. A swipe that settled open before the cancel ran
161
+ has already reported `onSwipeEnd("open")` (without `autoClose`).
162
+ - **Instant close:** `close(false)` clears cached open intent. Reattaching or
163
+ restoring that key must not resurrect the old open state.
164
+ - **Restoration and widths:** restore the cached physical edge at its current
165
+ width. If it is disabled, restore closed rather than the opposite edge.
166
+ Restoration/width reconciliation is not a new swipe and must not be used as a
167
+ synthetic start/end signal.
168
+ - **Completion:** aggregate `onSwipeEnd(state)` precedes the matching side
169
+ callback. Do not wire both to the same business action unless two invocations
170
+ are intended. A successful auto-close gesture reports `'open'` intent after
171
+ returning visually closed; that does not mean the cache remains open.
172
+ - **Raw direct gestures:** the direct native gesture path now shares activation,
173
+ threshold and auto-close handling with the canonical engine; it is not the old
174
+ single-side shortcut.
175
+
176
+ The appearance/haptic options are additive on this breaking base. They work with
177
+ one or both explicit sides; they do not restore removed API inputs or behaviors.
178
+
179
+ ## RTL and app migration checklist
180
+
181
+ 1. Map logical start/end actions to physical left/right in your app. RTL does not
182
+ exchange the library's sides, widths, command arguments or side callbacks.
183
+ 2. Update examples, wrappers, mocks and type fixtures to complete explicit pairs.
184
+ 3. Keep action padding/gaps inside the declared width and preserve usable targets.
185
+ Give each row a path to its actions without a swipe, with `accessibilityActions`
186
+ on the row content. See [Accessibility](../README.md#accessibility).
187
+ 4. Keep recycling keys stable through reorder, translation updates and reattachment.
188
+ 5. Rebuild iOS/Android and rerun application behavior, including close/reset,
189
+ recycling, disabled edges, child gestures, chat auto-close and RTL.
190
+ 6. Treat earlier physical checks and pre-migration CI as historical evidence,
191
+ not acceptance of the new binary.
@@ -0,0 +1,90 @@
1
+ # Support and tested configurations
2
+
3
+ This page states what 1.0 was tested on. A configuration that is not listed here
4
+ can work, but nobody has checked it.
5
+
6
+ ## Requirements
7
+
8
+ The package is an Expo module with native code.
9
+
10
+ - It needs a native build with Expo Modules: an Expo development build, `expo run`,
11
+ or a bare React Native app with `install-expo-modules`.
12
+ - It needs the New Architecture (Fabric) on iOS and Android. The iOS view does not
13
+ compile on the legacy architecture (Paper).
14
+ - It needs Expo SDK 54 or newer, React Native 0.81 or newer and React 19.1 or newer.
15
+ - Expo Go cannot load it.
16
+ - Rebuild the iOS and Android apps after every install or upgrade. A JavaScript
17
+ reload or an OTA update cannot change native code.
18
+
19
+ ## iOS 27 and the UIScene lifecycle
20
+
21
+ An app built with the iOS 27 SDK (Xcode 27) stops at launch on iOS 27 if it does not
22
+ adopt the UIScene lifecycle. iOS sets this requirement for the app. The Expo SDK 54
23
+ app template does not meet it. This package does not cause it and cannot fix it.
24
+
25
+ - The Expo SDK 54 template creates its window in the app delegate. It has no scene
26
+ delegate.
27
+ - The example app adopts the UIScene lifecycle with a local config plugin,
28
+ `example/plugins/with-scene-lifecycle.js`.
29
+ - Built with Xcode 27, the example app launched on a physical iPhone 11 Pro Max with
30
+ iOS 27.0.1. Only the launch was checked. The E2E suites did not run on iOS 27.
31
+
32
+ ## Declared ranges
33
+
34
+ The package declares these ranges. They are minimums, not a tested matrix.
35
+
36
+ | Declaration | Value |
37
+ | --- | --- |
38
+ | `expo` peer | `>=54.0.0` |
39
+ | `react` peer | `>=19.1.0` |
40
+ | `react-native` peer | `>=0.81.0` |
41
+ | Architecture | New Architecture (Fabric) only |
42
+ | iOS deployment target | 13.4 |
43
+ | Android `minSdkVersion` | 23 |
44
+
45
+ ## Tested for 1.0
46
+
47
+ Every configuration below ran the release candidate. The E2E suite has 62 cases
48
+ (25 single-sided, 19 two-sided, 14 RTL, 3 swipe to go back, 1 performance) plus the
49
+ Maestro flows.
50
+
51
+ | Area | Configuration | What ran |
52
+ | --- | --- | --- |
53
+ | App stack | Expo SDK 54.0, React Native 0.81.5, React 19.1.0 | Example app and a clean app that installs the packed tarball |
54
+ | Architecture | New Architecture (Fabric), Hermes | All runs below |
55
+ | Android | API 34 x86_64 emulator (Pixel 6 profile) | 62 E2E cases, Robolectric unit tests |
56
+ | iOS | iOS 26.4 simulator (iPhone 17 Pro), Xcode 26.6 | 62 E2E cases, Maestro flows, Swift unit tests |
57
+ | Packaging | `npm pack` tarball in a clean Expo app | Install, type check, autolinking, release builds on both platforms |
58
+
59
+ The Swift unit tests run with `swift test` on macOS in the `test-ios` CI job. They
60
+ cover the platform-independent helpers and state logic: swipe physics, side
61
+ selection, gesture completion, the open-state cache, child roles, rail visibility
62
+ and haptic state. They do not load UIKit or the native view. The E2E suites cover
63
+ the UIKit view on the simulator.
64
+
65
+ ## Not supported
66
+
67
+ - The legacy architecture (Paper). The iOS view does not compile on it, and no
68
+ Android run used it.
69
+ - Expo SDK below 54, React Native below 0.81 and React below 19.1. The peer ranges
70
+ exclude them.
71
+
72
+ ## Not tested
73
+
74
+ - iOS versions below the simulator runtime above, and Android API levels other than 34.
75
+ - Physical Android devices for 1.0. Earlier 0.x benchmarks used a Pixel 8 Pro.
76
+ - Automated tests of how haptics feel and how Reduce Motion behaves on a device.
77
+ The maintainer checks them by hand on a physical iPhone. The check runs when no
78
+ earlier check is recorded, and after a change to the iOS code, the haptics,
79
+ appearance or Reduce Motion code, or the example app's React Native or Expo
80
+ version.
81
+
82
+ If you run the package on another configuration, an issue with the result helps,
83
+ whether it works or not.
84
+
85
+ ## Reporting a problem
86
+
87
+ Open an issue at
88
+ <https://github.com/chocky335/react-native-swipeable-actions/issues> with the
89
+ package version, Expo and React Native versions, the architecture, and the OS and
90
+ device.
@@ -25,3 +25,35 @@ enum SwipeAnimationConfig {
25
25
  static let velocityProjectionFactor: CGFloat = 0.15
26
26
  static let progressChangeThreshold: CGFloat = 0.001
27
27
  }
28
+
29
+ extension SwipeAnimationConfig {
30
+ static let actionRailEntryScale: CGFloat = 0.96
31
+ static let actionRailPopScale: CGFloat = 1.03
32
+ static let actionRailRestScale: CGFloat = 1.0
33
+ static let actionRailPopRiseDuration: TimeInterval = 0.12
34
+ static let actionRailPopSettleDuration: TimeInterval = 0.18
35
+
36
+ static func actionRailScale(
37
+ progress: CGFloat, popElapsed: TimeInterval?, hasPopped: Bool, reduceMotion: Bool
38
+ ) -> CGFloat {
39
+ guard !reduceMotion else { return actionRailRestScale }
40
+ let entryScale = actionRailEntryScale + (actionRailRestScale - actionRailEntryScale) * max(0, min(1, progress))
41
+ if let elapsed = popElapsed {
42
+ if elapsed < actionRailPopRiseDuration {
43
+ // Rise from the scale shown at the crossing, so the pop starts without a jump.
44
+ let t = CGFloat(max(0, elapsed) / actionRailPopRiseDuration)
45
+ return entryScale + (actionRailPopScale - entryScale) * t
46
+ }
47
+ let t = CGFloat(min(1, (elapsed - actionRailPopRiseDuration) / actionRailPopSettleDuration))
48
+ return actionRailPopScale + (actionRailRestScale - actionRailPopScale) * (1 - (1 - t) * (1 - t))
49
+ }
50
+ if hasPopped { return actionRailRestScale }
51
+ return entryScale
52
+ }
53
+
54
+ /// Rails span the row and align actions to their outer edge. Scaling about
55
+ /// that edge, not the rail center, keeps a pop from pushing actions past the row.
56
+ static func actionRailTranslation(offset: CGFloat, scale: CGFloat, railWidth: CGFloat, side: SwipeSide) -> CGFloat {
57
+ offset - side.sign * railWidth / 2 * (1 - scale)
58
+ }
59
+ }