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
|
@@ -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.
|
package/docs/support.md
ADDED
|
@@ -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
|
+
}
|