forked from sheetjs/docs.sheetjs.com
1486 lines
41 KiB
Markdown
1486 lines
41 KiB
Markdown
---
|
||
title: Sheets on the Go with React Native
|
||
sidebar_label: React Native
|
||
description: Build data-intensive mobile apps with React Native. Seamlessly integrate spreadsheets into your app using SheetJS. Securely process and generate Excel files in the field.
|
||
pagination_prev: demos/static/index
|
||
pagination_next: demos/desktop/index
|
||
sidebar_position: 1
|
||
sidebar_custom_props:
|
||
summary: React + Native Rendering
|
||
---
|
||
|
||
import current from '/version.js';
|
||
import Tabs from '@theme/Tabs';
|
||
import TabItem from '@theme/TabItem';
|
||
import CodeBlock from '@theme/CodeBlock';
|
||
|
||
export const r = {style: {color:"red"}};
|
||
export const g = {style: {color:"green"}};
|
||
export const y = {style: {color:"gold"}};
|
||
export const gr = {style: {color:"gray"}};
|
||
|
||
[React Native](https://reactnative.dev/) is a mobile app framework. It builds
|
||
iOS and Android apps that use JavaScript for describing layouts and events.
|
||
|
||
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
|
||
data from spreadsheets.
|
||
|
||
This demo uses React Native and SheetJS to process and generate spreadsheets.
|
||
We'll explore how to load SheetJS in a React Native app in a few ways:
|
||
|
||
- ["Fetching Remote Data"](#fetching-remote-data) uses the built-in `fetch` to
|
||
download and parse remote workbook files.
|
||
- ["Local Files"](#local-files) uses native libraries to read and write files on
|
||
the device.
|
||
|
||
The "Local Files" example creates an app that looks like the screenshots below:
|
||
|
||
<table><thead><tr>
|
||
<th><a href="#demo">iOS</a></th>
|
||
<th><a href="#demo">Android</a></th>
|
||
</tr></thead><tbody><tr><td>
|
||
|
||

|
||
|
||
</td><td>
|
||
|
||

|
||
|
||
</td></tr></tbody></table>
|
||
|
||
:::caution pass
|
||
|
||
**Before testing this demo, follow the official React Native CLI Guide!**[^1]
|
||
|
||
Follow the instructions for iOS (requires macOS) and for Android. They will
|
||
cover installation and system configuration. You should be able to build and run
|
||
a sample app in the Android and the iOS (if applicable) simulators.
|
||
|
||
:::
|
||
|
||
## Integration Details
|
||
|
||
The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be
|
||
imported from any component or script in the app.
|
||
|
||
### Internal State
|
||
|
||
For simplicity, this demo uses an "Array of Arrays"[^2] as the internal state.
|
||
|
||
<table>
|
||
<thead><tr><th>Spreadsheet</th><th>Array of Arrays</th></tr></thead>
|
||
<tbody><tr><td>
|
||
|
||

|
||
|
||
</td><td>
|
||
|
||
```js
|
||
[
|
||
["Name", "Index"],
|
||
["Bill Clinton", 42],
|
||
["GeorgeW Bush", 43],
|
||
["Barack Obama", 44],
|
||
["Donald Trump", 45],
|
||
["Joseph Biden", 46]
|
||
]
|
||
```
|
||
|
||
</td></tr></tbody></table>
|
||
|
||
Each array within the structure corresponds to one row.
|
||
|
||
This demo also keeps track of the column widths as a single array of numbers.
|
||
The widths are used by the display component.
|
||
|
||
_Complete State_
|
||
|
||
The complete state is initialized with the following snippet:
|
||
|
||
```js
|
||
const [data, setData] = useState([
|
||
"SheetJS".split(""),
|
||
[5,4,3,3,7,9,5],
|
||
[8,6,7,5,3,0,9]
|
||
]);
|
||
const [widths, setWidths] = useState(Array.from({length:7}, () => 20));
|
||
```
|
||
|
||
|
||
#### Updating State
|
||
|
||
Starting from a SheetJS worksheet object, `sheet_to_json`[^3] with the `header`
|
||
option can generate an array of arrays:
|
||
|
||
```js
|
||
/* assuming `wb` is a SheetJS workbook */
|
||
function update_state(wb) {
|
||
/* convert first worksheet to AOA */
|
||
const wsname = wb.SheetNames[0];
|
||
const ws = wb.Sheets[wsname];
|
||
const data = utils.sheet_to_json(ws, {header:1});
|
||
|
||
/* update state */
|
||
setData(data);
|
||
|
||
/* update column widths */
|
||
setWidths(make_width(data));
|
||
}
|
||
```
|
||
|
||
_Calculating Column Widths_
|
||
|
||
Column widths can be calculated by walking each column and calculating the max
|
||
data width. Using the array of arrays:
|
||
|
||
```js
|
||
/* this function takes an array of arrays and generates widths */
|
||
function make_width(aoa) {
|
||
/* walk each row */
|
||
aoa.forEach((r) => {
|
||
/* walk each column */
|
||
r.forEach((c, C) => {
|
||
/* update column width based on the length of the cell contents */
|
||
res[C] = Math.max(res[C]||60, String(c).length * 10);
|
||
});
|
||
});
|
||
/* use a default value for columns with no data */
|
||
for(let C = 0; C < res.length; ++C) if(!res[C]) res[C] = 60;
|
||
return res;
|
||
}
|
||
```
|
||
|
||
#### Exporting State
|
||
|
||
`aoa_to_sheet`[^4] builds a SheetJS worksheet object from the array of arrays:
|
||
|
||
```js
|
||
/* generate a SheetJS workbook from the state */
|
||
function export_state() {
|
||
/* convert AOA back to worksheet */
|
||
const ws = utils.aoa_to_sheet(data);
|
||
|
||
/* build new workbook */
|
||
const wb = utils.book_new();
|
||
utils.book_append_sheet(wb, ws, "SheetJS");
|
||
|
||
return wb;
|
||
}
|
||
```
|
||
|
||
### Displaying Data
|
||
|
||
The demos uses `react-native-table-component` to display the first worksheet.
|
||
|
||
The demos use components similar to the example below:
|
||
|
||
```jsx
|
||
import { ScrollView } from 'react-native';
|
||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
||
(
|
||
{/* Horizontal scroll */}
|
||
<ScrollView horizontal={true} >
|
||
{/* Table container */}
|
||
<Table>
|
||
{/* Frozen Header Row */}
|
||
<TableWrapper>
|
||
{/* First row */}
|
||
<Row data={data[0]} widthArr={widths}/>
|
||
</TableWrapper>
|
||
{/* Scrollable Data Rows */}
|
||
<ScrollView>
|
||
<TableWrapper>
|
||
{/* Remaining Rows */}
|
||
<Rows data={data.slice(1)} widthArr={widths}/>
|
||
</TableWrapper>
|
||
</ScrollView>
|
||
</Table>
|
||
</ScrollView>
|
||
)
|
||
```
|
||
|
||
`data.slice(1)` in the `Rows` component returns data starting from the second
|
||
row. This neatly skips the first header row.
|
||
|
||
## Fetching Remote Data
|
||
|
||
React Native versions starting from `0.72.0`[^5] support binary data in `fetch`.
|
||
|
||
This snippet downloads and parses https://sheetjs.com/pres.xlsx:
|
||
|
||
```js
|
||
/* fetch data into an ArrayBuffer */
|
||
const ab = await (await fetch("https://sheetjs.com/pres.xlsx")).arrayBuffer();
|
||
/* parse data */
|
||
const wb = XLSX.read(ab);
|
||
```
|
||
|
||
### Fetch Demo
|
||
|
||
:::note Tested Deployments
|
||
|
||
This demo was tested in the following environments:
|
||
|
||
**Simulators**
|
||
|
||
| OS | Device | RN | Dev Platform | Date |
|
||
|:-----------|:------------------|:---------|:-------------|:-----------|
|
||
| Android 34 | Pixel 3a | `0.73.6` | `darwin-x64` | 2024-03-13 |
|
||
| iOS 17.4 | iPhone 15 Pro Max | `0.73.6` | `darwin-x64` | 2024-03-13 |
|
||
| Android 34 | Pixel 3a | `0.73.5` | `win10-x64` | 2024-03-05 |
|
||
|
||
**Real Devices**
|
||
|
||
| OS | Device | RN | Date |
|
||
|:-----------|:------------------|:---------|:-----------|
|
||
| iOS 15.1 | iPhone 12 Pro Max | `0.73.6` | 2024-03-13 |
|
||
| Android 29 | NVIDIA Shield | `0.73.6` | 2024-03-13 |
|
||
|
||
:::
|
||
|
||
1) Create project:
|
||
|
||
```bash
|
||
npx -y react-native@0.73.6 init SheetJSRNFetch --version="0.73.6"
|
||
```
|
||
|
||
2) Install shared dependencies:
|
||
|
||
<CodeBlock language="bash">{`\
|
||
cd SheetJSRNFetch
|
||
curl -LO https://docs.sheetjs.com/logo.png
|
||
npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
|
||
npm i -S react-native-table-component@1.2.0 @types/react-native-table-component`}
|
||
</CodeBlock>
|
||
|
||
3) Download [`App.tsx`](pathname:///reactnative/App.tsx) and replace:
|
||
|
||
```bash
|
||
curl -LO https://docs.sheetjs.com/reactnative/App.tsx
|
||
```
|
||
|
||
**Android Testing**
|
||
|
||
4) Install or switch to Java 17[^6]
|
||
|
||
:::note pass
|
||
|
||
When the demo was last tested on macOS, `java -version` displayed the following:
|
||
|
||
```
|
||
openjdk version "17.0.10" 2024-01-16
|
||
OpenJDK Runtime Environment Temurin-17.0.10+7 (build 17.0.10+7)
|
||
OpenJDK 64-Bit Server VM Temurin-17.0.10+7 (build 17.0.10+7, mixed mode)
|
||
```
|
||
|
||
:::
|
||
|
||
5) Start the Android emulator:
|
||
|
||
```bash
|
||
npx react-native run-android
|
||
```
|
||
|
||
:::caution pass
|
||
|
||
If the initial launch fails with an error referencing the emulator, manually
|
||
start the emulator from Android Studio and try again.
|
||
|
||
Gradle errors typically stem from a Java version mismatch:
|
||
|
||
```
|
||
> Failed to apply plugin 'com.android.internal.application'.
|
||
> Android Gradle plugin requires Java 17 to run. You are currently using Java 11.
|
||
```
|
||
|
||
This error can be resolved by installing and switching to the requested version.
|
||
|
||
:::
|
||
|
||
6) When opened, the app should look like the "Before" screenshot below. After
|
||
tapping "Import data from a spreadsheet", verify that the app shows new data:
|
||
|
||
<table><thead><tr>
|
||
<th>Before</th>
|
||
<th>After</th>
|
||
</tr></thead><tbody><tr><td>
|
||
|
||

|
||
|
||
</td><td>
|
||
|
||

|
||
|
||
</td></tr></tbody></table>
|
||
|
||
**iOS Testing**
|
||
|
||
:::danger pass
|
||
|
||
**iOS testing can only be performed on Apple hardware running macOS!**
|
||
|
||
Xcode and iOS simulators are not available on Windows or Linux.
|
||
|
||
:::
|
||
|
||
7) Refresh iOS project by running `pod install` from the `ios` subfolder:
|
||
|
||
```bash
|
||
cd ios; pod install; cd -
|
||
```
|
||
|
||
8) Start the iOS emulator:
|
||
|
||
```bash
|
||
npx react-native run-ios
|
||
```
|
||
|
||
9) When opened, the app should look like the "Before" screenshot below. After
|
||
tapping "Import data from a spreadsheet", verify that the app shows new data:
|
||
|
||
<table><thead><tr>
|
||
<th>Before</th>
|
||
<th>After</th>
|
||
</tr></thead><tbody><tr><td>
|
||
|
||

|
||
|
||
</td><td>
|
||
|
||

|
||
|
||
</td></tr></tbody></table>
|
||
|
||
**Android Device Testing**
|
||
|
||
10) Connect an Android device using a USB cable.
|
||
|
||
If the device asks to allow USB debugging, tap "Allow".
|
||
|
||
11) Close any Android / iOS emulators.
|
||
|
||
12) Build APK and run on device:
|
||
|
||
```bash
|
||
npx react-native run-android
|
||
```
|
||
|
||
**iOS Device Testing**
|
||
|
||
13) Connect an iOS device using a USB cable.
|
||
|
||
If the device asks to trust the computer, tap "Trust" and enter the passcode.
|
||
|
||
14) Close any Android / iOS emulators.
|
||
|
||
15) Enable developer code signing certificates[^7].
|
||
|
||
<details open>
|
||
<summary><b>Enabling Code Signing</b> (click to show)</summary>
|
||
|
||
These instructions were verified against Xcode 15.3.
|
||
|
||
A) Open the included iOS workspace in Xcode:
|
||
|
||
```bash
|
||
open ios/SheetJSRNFetch.xcworkspace
|
||
```
|
||
|
||
B) Select "SheetJSRNFetch" in the Project Navigator:
|
||
|
||

|
||
|
||
C) Select "Signing & Capabilities" in the main view.
|
||
|
||
D) Select "All" in the lower bar and ensure a Team is selected in the dropdown:
|
||
|
||

|
||
|
||
</details>
|
||
|
||
16) Install `ios-deploy` through Homebrew:
|
||
|
||
```bash
|
||
brew install ios-deploy
|
||
```
|
||
|
||
17) Run on device:
|
||
|
||
```bash
|
||
npx react-native run-ios
|
||
```
|
||
|
||
:::caution pass
|
||
|
||
In some tests, the app failed to run on the device due to "Untrusted Developer":
|
||
|
||
```
|
||
Your device management settings do not allow apps from developer ... on this iPhone. You can allow using these apps in Settings.
|
||
```
|
||
|
||
These instructions were verified against iOS 15.1.
|
||
|
||
A) Open the Settings app and select "General" > "VPN & Device Management".
|
||
|
||

|
||
|
||
B) Select the Apple Development certificate under "Developer App".
|
||
|
||

|
||
|
||
In the new screen, the name "SheetJSRNFetch" will be displayed in the Apps list.
|
||
|
||
C) Tap "Trust ..." and tap "Trust" in the popup.
|
||
|
||

|
||
|
||
D) Close the Settings app on the device.
|
||
|
||
E) Close the Metro window on the computer.
|
||
|
||
F) Run `npx react-native run-ios` again.
|
||
|
||
:::
|
||
|
||
:::note pass
|
||
|
||
In some tests, the build failed with the following error:
|
||
|
||
```
|
||
PhaseScriptExecution failed with a nonzero exit code
|
||
```
|
||
|
||
This was due to an error in the `react-native` package. The script
|
||
`node_modules/react-native/scripts/react-native-xcode.sh` must be edited.
|
||
|
||
Near the top of the script, there will be a `set` statement:
|
||
|
||
```bash title="node_modules/react-native/scripts/react-native-xcode.sh"
|
||
# Print commands before executing them (useful for troubleshooting)
|
||
# highlight-next-line
|
||
set -x -e
|
||
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH
|
||
```
|
||
|
||
The `-e` argument must be removed:
|
||
|
||
```bash title="node_modules/react-native/scripts/react-native-xcode.sh (edit line)"
|
||
# Print commands before executing them (useful for troubleshooting)
|
||
# highlight-next-line
|
||
set -x
|
||
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH
|
||
```
|
||
|
||
:::
|
||
|
||
:::caution pass
|
||
|
||
In some test runs, the error mentioned a development team:
|
||
|
||
```
|
||
error: Signing for "SheetJSRNFetch" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'SheetJSRNFetch' from project 'SheetJSRNFetch')
|
||
```
|
||
|
||
Code signing must be enabled in the project!
|
||
|
||
:::
|
||
|
||
:::info pass
|
||
|
||
By default, React Native generates applications that exclusively target iPhone.
|
||
On a physical iPad, a pixellated iPhone app will be run.
|
||
|
||
The "targeted device families" setting must be changed to support iPad:
|
||
|
||
A) Open the Xcode workspace:
|
||
|
||
```bash
|
||
open ./ios/SheetJSRNFetch.xcworkspace
|
||
```
|
||
|
||
B) Select the project in the left sidebar:
|
||
|
||

|
||
|
||
C) Select the "SheetJSRNFetch" target in the sidebar.
|
||
|
||

|
||
|
||
D) Select the "Build Settings" tab in the main area.
|
||
|
||
E) In the search bar below "Build Settings", type "tar"
|
||
|
||
F) In the "Targeted Device Families" row, change the value to "iPhone, iPad".
|
||
|
||
:::
|
||
|
||
:::caution pass
|
||
|
||
In some test runs, the build failed with a Provisioning message:
|
||
|
||
```
|
||
error: Provisioning profile "..." doesn't include the currently selected device "..." (identifier ...).
|
||
```
|
||
|
||
This was resolved by manually selecting the target device:
|
||
|
||
A) Open the Xcode workspace:
|
||
|
||
```bash
|
||
open ./ios/SheetJSRNFetch.xcworkspace
|
||
```
|
||
|
||
B) Select the project in the left sidebar:
|
||
|
||

|
||
|
||
C) In the top bar, next to the project name, there will be a gray device icon.
|
||
Click on the icon and select the real device from the list.
|
||
|
||
:::
|
||
|
||
## Local Files
|
||
|
||
:::danger pass
|
||
|
||
React Native does not provide a native file picker or a method for reading and
|
||
writing data from documents on the devices. A third-party library must be used.
|
||
|
||
Since React Native internals change between releases, libraries may only work
|
||
with specific versions of React Native. Project documentation should be
|
||
consulted before picking a library.
|
||
|
||
:::
|
||
|
||
The following table lists tested file plugins. "OS" lists tested platforms
|
||
("A" for Android and "I" for iOS).
|
||
|
||
| File system Plugin | File Picker Plugin | OS |
|
||
|:---------------------------|:-------------------------------|:----:|
|
||
| `react-native-file-access` | `react-native-document-picker` | `AI` |
|
||
| `react-native-blob-util` | `react-native-document-picker` | `AI` |
|
||
| `expo-file-system` | `expo-document-picker` | `AI` |
|
||
|
||
### App Configuration
|
||
|
||
Due to privacy concerns, apps must request file access. There are special APIs
|
||
for accessing data and are subject to change in future platform versions.
|
||
|
||
<details>
|
||
<summary><b>Technical Details</b> (click to show)</summary>
|
||
|
||
**iOS**
|
||
|
||
iOS applications typically require two special settings in `Info.plist`:
|
||
|
||
- `UIFileSharingEnabled`[^8] allows users to use files written by the app. A
|
||
special folder will appear in the "Files" app.
|
||
|
||
- `LSSupportsOpeningDocumentsInPlace`[^9] allows the app to open files without
|
||
creating a local copy.
|
||
|
||
Both settings must be set to `true`:
|
||
|
||
```xml title="Info.plist (add to file)"
|
||
<plist version="1.0">
|
||
<dict>
|
||
<!-- highlight-start -->
|
||
<key>UIFileSharingEnabled</key>
|
||
<true/>
|
||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||
<true/>
|
||
<!-- highlight-end -->
|
||
<key>CFBundleDevelopmentRegion</key>
|
||
```
|
||
|
||
Once the options are set, generated files are visible to users and can be shared
|
||
with other apps including "Mail", "Messages", and "Numbers".
|
||
|
||
**Android**
|
||
|
||
Permissions and APIs have evolved over time. For broadest compatibility, the
|
||
following permissions must be enabled in `AndroidManifest.xml`:
|
||
|
||
- `READ_EXTERNAL_STORAGE` and `WRITE_EXTERNAL_STORAGE` allow apps to access
|
||
files outside of the app scope. These are required for scoped storage access.
|
||
|
||
- `android:requestLegacyExternalStorage="true"` enabled legacy behavior in some
|
||
older releases.
|
||
|
||
The manifest is saved to `android/app/src/main/AndroidManifest.xml`:
|
||
|
||
```xml title="android/app/src/main/AndroidManifest.xml (add highlighted lines)"
|
||
<uses-permission android:name="android.permission.INTERNET" />
|
||
<!-- highlight-start -->
|
||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||
<!-- highlight-end -->
|
||
<application
|
||
<!-- highlight-next-line -->
|
||
android:requestLegacyExternalStorage="true"
|
||
android:name=".MainApplication"
|
||
android:label="@string/app_name"
|
||
android:icon="@mipmap/ic_launcher"
|
||
android:roundIcon="@mipmap/ic_launcher_round"
|
||
android:allowBackup="false"
|
||
android:theme="@style/AppTheme">
|
||
```
|
||
|
||
Depending on the Android API level, there are three strategies for writing files:
|
||
|
||
- In "legacy" mode (supported in API levels up to 29), files can be written to
|
||
the user Downloads or Documents folder directly.
|
||
|
||
- Using the `MediaStore` API, files should be copied to a visible location.
|
||
|
||
- Using the "Storage Access Framework", the user grants access to a folder and
|
||
the app uses SAF APIs to create files and write data.
|
||
|
||
</details>
|
||
|
||
### RN File Picker
|
||
|
||
The "File Picker" library handles two platform-specific steps:
|
||
|
||
1) Show a view that allows users to select a file from their device
|
||
|
||
2) Copy the selected file to a location that can be read by the application
|
||
|
||
The following libraries have been tested:
|
||
|
||
#### `react-native-document-picker`
|
||
|
||
[`react-native-document-picker`](https://react-native-documents.github.io/)
|
||
provides a `pickSingle` method for users to select one file.
|
||
|
||
The file plugins generally require the `copyTo: "cachesDirectory"` option:
|
||
|
||
```js
|
||
import { pickSingle } from 'react-native-document-picker';
|
||
|
||
const f = await pickSingle({
|
||
allowMultiSelection: false,
|
||
// highlight-next-line
|
||
copyTo: "cachesDirectory",
|
||
mode: "open"
|
||
});
|
||
const path = f.fileCopyUri; // this path can be read by RN file plugins
|
||
```
|
||
|
||
#### `expo-document-picker`
|
||
|
||
[`expo-document-picker`](https://docs.expo.dev/sdk/document-picker/) is a picker
|
||
that works with other modules in the Expo ecosystem.
|
||
|
||
The `getDocumentAsync` method allows users to select a file. The Expo file
|
||
plugin requires the `copyToCacheDirectory` option:
|
||
|
||
```js
|
||
import * as DocumentPicker from 'expo-document-picker';
|
||
|
||
const result = await DocumentPicker.getDocumentAsync({
|
||
// highlight-next-line
|
||
copyToCacheDirectory: true,
|
||
type: ['application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
|
||
});
|
||
const path = result.assets[0].uri; // this path can be read by RN file plugins
|
||
```
|
||
|
||
|
||
### RN File Plugins
|
||
|
||
The following libraries have been tested:
|
||
|
||
#### `react-native-blob-util`
|
||
|
||
[`react-native-blob-util`](https://github.com/RonRadtke/react-native-blob-util)
|
||
is the continuation of other libraries that date back to 2016.
|
||
|
||
The `ascii` type returns an array of numbers corresponding to the raw bytes.
|
||
A `Uint8Array` from the data is compatible with the `buffer` type.
|
||
|
||
<details>
|
||
<summary><b>Reading and Writing snippets</b> (click to hide)</summary>
|
||
|
||
_Reading Data_
|
||
|
||
```js
|
||
import * as XLSX from "xlsx";
|
||
import RNFetchBlob from 'react-native-blob-util';
|
||
const { readFile } = RNFetchBlob.fs;
|
||
|
||
const res = await readFile(path, 'ascii');
|
||
const wb = XLSX.read(new Uint8Array(res), {type:'buffer'});
|
||
```
|
||
|
||
:::caution pass
|
||
|
||
On iOS, the URI from `react-native-document-picker` must be massaged:
|
||
|
||
```js
|
||
import { pickSingle } from 'react-native-document-picker';
|
||
import RNFetchBlob from 'react-native-blob-util';
|
||
const { readFile, dirs: { DocumentDir } } = RNFetchBlob.fs;
|
||
|
||
const f = await pickSingle({
|
||
// highlight-start
|
||
// Instruct the document picker to copy file to Documents directory
|
||
copyTo: "documentDirectory",
|
||
// highlight-end
|
||
allowMultiSelection: false, mode: "open" });
|
||
// highlight-start
|
||
// `f.uri` is the original path and `f.fileCopyUri` is the path to the copy
|
||
let path = f.fileCopyUri;
|
||
// iOS workaround
|
||
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, DDP + "/");
|
||
// highlight-end
|
||
|
||
const res = await readFile(path, 'ascii');
|
||
```
|
||
|
||
:::
|
||
|
||
_Writing Data_
|
||
|
||
```js
|
||
import * as XLSX from "xlsx";
|
||
import RNFetchBlob from 'react-native-blob-util';
|
||
const { writeFile, readFile, dirs:{ DocumentDir } } = RNFetchBlob.fs;
|
||
|
||
const wbout = XLSX.write(wb, {type:'buffer', bookType:"xlsx"});
|
||
const file = DocumentDir + "/sheetjsw.xlsx";
|
||
const res = await writeFile(file, Array.from(wbout), 'ascii');
|
||
```
|
||
|
||
_Sharing Files in Android_
|
||
|
||
`copyToMediaStore` uses the `MediaStore` API to share files.
|
||
|
||
The file must be written to the device *before* using the `MediaStore` API!
|
||
|
||
```js
|
||
// ... continuation of "writing data"
|
||
const { MediaCollection } = RNFetchBlob;
|
||
|
||
/* Copy to downloads directory (android) */
|
||
try {
|
||
await MediaCollection.copyToMediaStore({
|
||
parentFolder: "",
|
||
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
name: "sheetjsw.xlsx"
|
||
}, "Download", file);
|
||
} catch(e) {}
|
||
```
|
||
|
||
</details>
|
||
|
||
#### `react-native-file-access`
|
||
|
||
[`react-native-file-access`](https://github.com/alpha0010/react-native-file-access)
|
||
is a filesystem API that uses modern iOS and Android development patterns.
|
||
|
||
The `base64` encoding returns strings compatible with the `base64` type:
|
||
|
||
<details>
|
||
<summary><b>Reading and Writing snippets</b> (click to hide)</summary>
|
||
|
||
_Reading Data_
|
||
|
||
```js
|
||
import * as XLSX from "xlsx";
|
||
import { FileSystem } from "react-native-file-access";
|
||
|
||
const b64 = await FileSystem.readFile(path, "base64");
|
||
/* b64 is a Base64 string */
|
||
const workbook = XLSX.read(b64, {type: "base64"});
|
||
```
|
||
|
||
_Writing Data_
|
||
|
||
```js
|
||
import * as XLSX from "xlsx";
|
||
import { Dirs, FileSystem } from "react-native-file-access";
|
||
const DDP = Dirs.DocumentDir + "/";
|
||
|
||
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
|
||
/* b64 is a Base64 string */
|
||
await FileSystem.writeFile(DDP + "sheetjs.xlsx", b64, "base64");
|
||
```
|
||
|
||
_Sharing Files in Android_
|
||
|
||
`cpExternal` uses the `MediaStore` API to share files.
|
||
|
||
The file must be written to the device *before* using the `MediaStore` API!
|
||
|
||
```js
|
||
// ... continuation of "writing data"
|
||
|
||
/* Copy to downloads directory (android) */
|
||
try {
|
||
await FileSystem.cpExternal(file, "sheetjsw.xlsx", "downloads");
|
||
} catch(e) {}
|
||
```
|
||
|
||
</details>
|
||
|
||
#### `expo-file-system`
|
||
|
||
[`expo-file-system`](https://docs.expo.dev/sdk/filesystem/) is a filesystem API
|
||
that works with other modules in the Expo ecosystem.
|
||
|
||
:::caution pass
|
||
|
||
Some Expo APIs return URI that cannot be read with `expo-file-system`. This
|
||
will manifest as an error:
|
||
|
||
> Unsupported scheme for location '...'
|
||
|
||
The [`expo-document-picker`](#expo-document-picker) snippet makes a local copy.
|
||
|
||
:::
|
||
|
||
The `EncodingType.Base64` encoding is compatible with `base64` type.
|
||
|
||
<details>
|
||
<summary><b>Reading and Writing snippets</b> (click to hide)</summary>
|
||
|
||
_Reading Data_
|
||
|
||
Calling `FileSystem.readAsStringAsync` with `FileSystem.EncodingType.Base64`
|
||
encoding returns a promise resolving to a string compatible with `base64` type:
|
||
|
||
```js
|
||
import * as XLSX from "xlsx";
|
||
import * as FileSystem from 'expo-file-system';
|
||
|
||
const b64 = await FileSystem.readAsStringAsync(uri, { encoding: FileSystem.EncodingType.Base64 });
|
||
const workbook = XLSX.read(b64, { type: "base64" });
|
||
```
|
||
|
||
_Writing Data_
|
||
|
||
The `FileSystem.EncodingType.Base64` encoding accepts Base64 strings:
|
||
|
||
```js
|
||
import * as XLSX from "xlsx";
|
||
import * as FileSystem from 'expo-file-system';
|
||
|
||
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
|
||
/* b64 is a Base64 string */
|
||
await FileSystem.writeAsStringAsync(FileSystem.documentDirectory + "sheetjs.xlsx", b64, { encoding: FileSystem.EncodingType.Base64 });
|
||
```
|
||
|
||
_Sharing Files in Android_
|
||
|
||
`StorageAccessFramework` uses the "Storage Access Framework" to share files.
|
||
|
||
SAF API methods must be used to request permissions, make files and write data:
|
||
|
||
```js
|
||
import * as XLSX from "xlsx";
|
||
import { documentDirectory, StorageAccessFramework } from 'expo-file-system';
|
||
|
||
const b64 = XLSX.write(workbook, {type:'base64', bookType:"xlsx"});
|
||
/* b64 is a Base64 string */
|
||
try {
|
||
/* request access to a folder */
|
||
const perms = await StorageAccessFramework.requestDirectoryPermissionsAsync(documentDirectory);
|
||
/* if the user selected a folder ... */
|
||
if(perms.granted) {
|
||
|
||
/* create a new file */
|
||
const uri = perms.directoryUri;
|
||
const file = await StorageAccessFramework.createFileAsync(uri, "sheetjsw", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||
|
||
/* write data to file */
|
||
await StorageAccessFramework.writeAsStringAsync(file, wbout, { encoding: "base64" });
|
||
}
|
||
} catch(e) {}
|
||
```
|
||
|
||
</details>
|
||
|
||
### Demo
|
||
|
||
:::note Tested Deployments
|
||
|
||
This demo was tested in the following environments:
|
||
|
||
**Simulators**
|
||
|
||
| OS | Device | RN | Dev Platform | Date |
|
||
|:-----------|:------------------|:---------|:-------------|:-----------|
|
||
| Android 34 | Pixel 3a | `0.73.6` | `darwin-x64` | 2024-03-31 |
|
||
| iOS 17.4 | iPhone 15 Pro Max | `0.73.6` | `darwin-x64` | 2024-03-31 |
|
||
| Android 34 | Pixel 3a | `0.73.6` | `win10-x64` | 2024-03-31 |
|
||
| Android 34 | Pixel 3a | `0.73.6` | `linux-x64` | 2024-03-31 |
|
||
|
||
**Real Devices**
|
||
|
||
| OS | Device | RN | Date |
|
||
|:-----------|:------------------|:---------|:-----------|
|
||
| iOS 15.5 | iPhone 13 Pro Max | `0.73.6` | 2024-03-31 |
|
||
| Android 29 | NVIDIA Shield | `0.73.6` | 2024-03-31 |
|
||
|
||
:::
|
||
|
||
:::danger pass
|
||
|
||
There are many moving parts and pitfalls with React Native apps. It is strongly
|
||
recommended to follow the official React Native tutorials for iOS and Android
|
||
before approaching this demo.[^10] Details including Android Virtual Device
|
||
configuration are not covered here.
|
||
|
||
:::
|
||
|
||
This example tries to separate the library-specific functions.
|
||
|
||
**Project Setup**
|
||
|
||
1) Create project:
|
||
|
||
```bash
|
||
npx react-native init SheetJSRN --version="0.73.6"
|
||
```
|
||
|
||
On macOS, if prompted to install `CocoaPods`, press `y`.
|
||
|
||
2) Install shared dependencies:
|
||
|
||
<CodeBlock language="bash">{`\
|
||
cd SheetJSRN
|
||
curl -LO https://docs.sheetjs.com/logo.png
|
||
npm i -S https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz
|
||
npm i -S react-native-table-component@1.2.0 react-native-document-picker@9.1.1`}
|
||
</CodeBlock>
|
||
|
||
3) Download [`index.js`](pathname:///mobile/index.js) and replace:
|
||
|
||
```bash
|
||
curl -LO https://docs.sheetjs.com/mobile/index.js
|
||
```
|
||
|
||
4) Start the Android emulator:
|
||
|
||
```bash
|
||
npx react-native run-android
|
||
```
|
||
|
||
The app should look like the following screenshot:
|
||
|
||

|
||
|
||
:::caution pass
|
||
|
||
When this demo was last tested on Windows, the build failed with an error:
|
||
|
||
```
|
||
> Failed to apply plugin 'com.android.internal.application'.
|
||
> Android Gradle plugin requires Java 17 to run. You are currently using Java 11.
|
||
```
|
||
|
||
Java 17 must be installed[^11] and the `JAVA_HOME` environment variable must
|
||
point to the Java 17 location.
|
||
|
||
:::
|
||
|
||
Stop the dev server and close the React Native Metro NodeJS window.
|
||
|
||
**File Integration**
|
||
|
||
5) Pick a filesystem library for integration:
|
||
|
||
<Tabs>
|
||
<TabItem value="RNBU" label="RNBU">
|
||
|
||
Install `react-native-blob-util` dependency:
|
||
|
||
```bash
|
||
npm i -S react-native-blob-util@0.19.8
|
||
```
|
||
|
||
Add the highlighted lines to `index.js`:
|
||
|
||
```js title="index.js"
|
||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
||
// highlight-start
|
||
import { read, write } from 'xlsx';
|
||
import { pickSingle } from 'react-native-document-picker';
|
||
import { Platform } from 'react-native';
|
||
import RNFetchBlob from 'react-native-blob-util';
|
||
|
||
async function pickAndParse() {
|
||
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
|
||
let path = f.fileCopyUri;
|
||
if (Platform.OS === 'ios') path = path.replace(/^.*\/Documents\//, RNFetchBlob.fs.dirs.DocumentDir + "/");
|
||
const res = await (await fetch(path)).arrayBuffer(); // RN >= 0.72
|
||
// const res = await RNFetchBlob.fs.readFile(path, 'ascii'); // RN < 0.72
|
||
return read(new Uint8Array(res), {type: 'buffer'});
|
||
}
|
||
|
||
async function writeWorkbook(wb) {
|
||
const wbout = write(wb, {type:'buffer', bookType:"xlsx"});
|
||
const file = RNFetchBlob.fs.dirs.DocumentDir + "/sheetjsw.xlsx";
|
||
await RNFetchBlob.fs.writeFile(file, Array.from(wbout), 'ascii');
|
||
|
||
/* Copy to downloads directory (android) */
|
||
try { await RNFetchBlob.MediaCollection.copyToMediaStore({
|
||
parentFolder: "",
|
||
mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||
name: "sheetjsw.xlsx"
|
||
}, "Download", file); } catch(e) {}
|
||
|
||
return file;
|
||
}
|
||
// highlight-end
|
||
|
||
const make_width = ws => {
|
||
```
|
||
|
||
</TabItem>
|
||
<TabItem value="RNFA" label="RNFA">
|
||
|
||
Install `react-native-file-access` dependency:
|
||
|
||
```bash
|
||
npm i -S react-native-file-access@3.0.7
|
||
```
|
||
|
||
Add the highlighted lines to `index.js`:
|
||
|
||
```js title="index.js"
|
||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
||
// highlight-start
|
||
import { read, write } from 'xlsx';
|
||
import { pickSingle } from 'react-native-document-picker';
|
||
import { Dirs, FileSystem } from 'react-native-file-access';
|
||
|
||
async function pickAndParse() {
|
||
const f = await pickSingle({allowMultiSelection: false, copyTo: "documentDirectory", mode: "open" });
|
||
let path = f.fileCopyUri;
|
||
const res = await (await fetch(path)).arrayBuffer();
|
||
return read(new Uint8Array(res), {type: 'buffer'});
|
||
}
|
||
|
||
async function writeWorkbook(wb) {
|
||
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
|
||
const file = Dirs.DocumentDir + "/sheetjsw.xlsx";
|
||
await FileSystem.writeFile(file, wbout, "base64");
|
||
|
||
/* Copy to downloads directory (android) */
|
||
try { await FileSystem.cpExternal(file, "sheetjsw.xlsx", "downloads"); } catch(e) {}
|
||
|
||
return file;
|
||
}
|
||
// highlight-end
|
||
|
||
const make_width = ws => {
|
||
```
|
||
|
||
</TabItem>
|
||
<TabItem value="EXPO" label="EXPO">
|
||
|
||
Install `expo-file-system` and `expo-document-picker` dependencies:
|
||
|
||
```bash
|
||
npx install-expo-modules
|
||
npm i -S expo-file-system expo-document-picker
|
||
```
|
||
|
||
:::note pass
|
||
|
||
In the most recent test, the installation asked a few questions.
|
||
|
||
If prompted to change iOS deployment target, choose Yes.
|
||
|
||
If prompted to install Expo CLI integration, choose No.
|
||
|
||
:::
|
||
|
||
Add the highlighted lines to `index.js`:
|
||
|
||
```js title="index.js"
|
||
import { Table, Row, Rows, TableWrapper } from 'react-native-table-component';
|
||
|
||
// highlight-start
|
||
import { read, write } from 'xlsx';
|
||
import { getDocumentAsync } from 'expo-document-picker';
|
||
import { documentDirectory, readAsStringAsync, writeAsStringAsync, StorageAccessFramework } from 'expo-file-system';
|
||
|
||
async function pickAndParse() {
|
||
const result = await getDocumentAsync({copyToCacheDirectory: true});
|
||
const path = result.assets[0].uri;
|
||
const res = await readAsStringAsync(path, { encoding: "base64" });
|
||
return read(res, {type: 'base64'});
|
||
}
|
||
|
||
async function writeWorkbook(wb) {
|
||
const wbout = write(wb, {type:'base64', bookType:"xlsx"});
|
||
const file = documentDirectory + "sheetjsw.xlsx";
|
||
await writeAsStringAsync(file, wbout, { encoding: "base64" });
|
||
|
||
/* Write to documents directory (android) */
|
||
try {
|
||
const perms = await StorageAccessFramework.requestDirectoryPermissionsAsync(documentDirectory);
|
||
if(perms.granted) {
|
||
const uri = perms.directoryUri;
|
||
const file = await StorageAccessFramework.createFileAsync(uri, "sheetjsw", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||
await StorageAccessFramework.writeAsStringAsync(file, wbout, { encoding: "base64" });
|
||
}
|
||
} catch(e) {}
|
||
return file;
|
||
}
|
||
// highlight-end
|
||
|
||
const make_width = ws => {
|
||
```
|
||
|
||
</TabItem>
|
||
</Tabs>
|
||
|
||
**Android Testing**
|
||
|
||
7) Restart the Android development process:
|
||
|
||
```bash
|
||
npx react-native run-android
|
||
```
|
||
|
||
The following app will be shown:
|
||
|
||

|
||
|
||
:::caution pass
|
||
|
||
When this demo was last tested on macOS, the process failed to launch the emulator:
|
||
|
||
<pre>
|
||
<b {...y}>warn</b> Please launch an emulator manually or connect a device. Otherwise app may fail to launch.
|
||
</pre>
|
||
|
||
|
||
**This is a known bug in React Native!**
|
||
|
||
If an emulator is installed, run the following command:
|
||
|
||
```bash
|
||
npx react-native doctor
|
||
```
|
||
|
||
Under `Android`, there will be one error:
|
||
|
||
<pre>
|
||
<span {...gr}>Android</span> {`\n`}
|
||
{` `}<span {...r}>✖</span> Adb - No devices and/or emulators connected. Please create emulator with Android Studio or connect Android device.
|
||
</pre>
|
||
|
||
Press `f` and a list of available emulators will be shown. Select the emulator
|
||
(typically the last line) and press Enter.
|
||
|
||
<pre>
|
||
<span {...g}>✔</span> <b>Select the device / emulator you want to use</b> <span {...gr}>›</span> <b>Emulator</b> <span {...g}>Pixel_3a_API_34</span> (disconnected)
|
||
</pre>
|
||
|
||
The text in green is the name of the virtual device (`Pixel_3a_API_34` in this
|
||
example). Run the following command to manually start the emulator:
|
||
|
||
```bash
|
||
$ANDROID_HOME/tools/emulator -avd Pixel_3a_API_34
|
||
```
|
||
|
||
(replace `Pixel_3a_API_34` with the name of the virtual device)
|
||
|
||
To confirm React Native detects the emulator, run the following command again:
|
||
|
||
```bash
|
||
npx react-native doctor
|
||
```
|
||
|
||
:::
|
||
|
||
8) Download https://sheetjs.com/pres.numbers and open the Downloads folder.
|
||
|
||
9) Click and drag `pres.numbers` from the Downloads folder into the simulator.
|
||
|
||
10) Click "Import data" and look for `pres.numbers`.
|
||
|
||
If the file is not displayed, click the `≡` icon and click "Downloads".
|
||
|
||

|
||
|
||
11) Select `pres.numbers`.
|
||
|
||
The screen should refresh with new contents:
|
||
|
||

|
||
|
||
12) Click "Export data".
|
||
|
||
:::note pass
|
||
|
||
`expo-file-system` on Android will prompt to grant access to a folder.
|
||
|
||
Tap the `≡` icon and tap the "Documents" folder with the download icon.
|
||
|
||
Tap the 'ALLOW ACCESS TO "DOCUMENTS"' button.
|
||
|
||
In the "Allow access" pop, tap "ALLOW".
|
||
|
||
:::
|
||
|
||
An alert will display the location to the file:
|
||
|
||

|
||
|
||
13) Pull the file from the simulator and verify the contents:
|
||
|
||
```bash
|
||
adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > /tmp/sheetjsw.xlsx
|
||
npx xlsx-cli /tmp/sheetjsw.xlsx
|
||
```
|
||
|
||
:::caution pass
|
||
|
||
PowerShell mangles binary data in the redirect.
|
||
|
||
On Windows, the following commands must be run in the Command Prompt:
|
||
|
||
```bash
|
||
adb exec-out run-as com.sheetjsrn cat files/sheetjsw.xlsx > sheetjsw.xlsx
|
||
npx xlsx-cli sheetjsw.xlsx
|
||
```
|
||
|
||
:::
|
||
|
||
14) Stop the dev server and close the React Native Metro NodeJS window.
|
||
|
||
**iOS Testing**
|
||
|
||
:::danger pass
|
||
|
||
**iOS testing can only be performed on Apple hardware running macOS!**
|
||
|
||
Xcode and iOS simulators are not available on Windows or Linux.
|
||
|
||
Scroll down to "Android Device Testing" for device tests.
|
||
|
||
:::
|
||
|
||
15) Refresh iOS project by running `pod install` from the `ios` subfolder:
|
||
|
||
```bash
|
||
cd ios; pod install; cd -
|
||
```
|
||
|
||
16) Start the iOS development process:
|
||
|
||
```bash
|
||
npx react-native run-ios
|
||
```
|
||
|
||
17) Download https://sheetjs.com/pres.numbers and open the Downloads folder.
|
||
|
||
18) In the simulator, click the Home icon to return to the home screen.
|
||
|
||
19) Click on the "Files" icon to open the app.
|
||
|
||
20) Click and drag `pres.numbers` from the Downloads folder into the simulator.
|
||
|
||

|
||
|
||
21) Make sure "On My iPhone" is highlighted and select "Save".
|
||
|
||
22) Click the Home icon again then select the `SheetJSRN` app.
|
||
|
||
23) Click "Import data" and select `pres`:
|
||
|
||

|
||
|
||
Once selected, the screen should refresh with new contents:
|
||
|
||

|
||
|
||
24) Click "Export data".
|
||
|
||
An alert will display the location to the file:
|
||
|
||

|
||
|
||
25) Find the file and verify the contents are correct:
|
||
|
||
```bash
|
||
find ~/Library/Developer/CoreSimulator -name sheetjsw.xlsx |
|
||
while read x; do echo "$x"; npx xlsx-cli "$x"; done
|
||
```
|
||
|
||
Once testing is complete, stop the simulator and the development process.
|
||
|
||
**Android Device Testing**
|
||
|
||
26) Add the highlighted lines to `android/app/src/main/AndroidManifest.xml`:
|
||
|
||
```xml title="android/app/src/main/AndroidManifest.xml (add highlighted lines)"
|
||
<uses-permission android:name="android.permission.INTERNET" />
|
||
<!-- highlight-start -->
|
||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||
<!-- highlight-end -->
|
||
<application
|
||
<!-- highlight-next-line -->
|
||
android:requestLegacyExternalStorage="true"
|
||
android:name=".MainApplication"
|
||
android:label="@string/app_name"
|
||
android:icon="@mipmap/ic_launcher"
|
||
android:roundIcon="@mipmap/ic_launcher_round"
|
||
android:allowBackup="false"
|
||
android:theme="@style/AppTheme">
|
||
```
|
||
|
||
There will be two new `uses-permission` tags within the parent `manifest` tag.
|
||
The attribute `android:requestLegacyExternalStorage="true"` must be added to the
|
||
`application` tag.
|
||
|
||
27) Close any Android / iOS simulators.
|
||
|
||
Stop the dev server and close the React Native Metro NodeJS window.
|
||
|
||
28) Connect an Android device using a USB cable.
|
||
|
||
If the device asks to allow USB debugging, tap "Allow".
|
||
|
||
29) Build APK and run on device:
|
||
|
||
```bash
|
||
npx react-native run-android
|
||
```
|
||
|
||
30) Download https://sheetjs.com/pres.numbers on the device.
|
||
|
||
31) Switch back to the "SheetJSRN" app.
|
||
|
||
32) Tap "Import data" and tap `pres.numbers`.
|
||
|
||
If the file is not displayed, tap the `≡` icon and tap "Downloads".
|
||
|
||
The table will refresh with data from the file.
|
||
|
||
33) Tap "Export Data".
|
||
|
||
:::note pass
|
||
|
||
`expo-file-system` on Android will prompt to grant access to a folder.
|
||
|
||
Tap the `≡` icon and tap the "Documents" folder with the download icon.
|
||
|
||
Tap the 'ALLOW ACCESS TO "DOCUMENTS"' button.
|
||
|
||
In the "Allow access" pop, tap "ALLOW".
|
||
|
||
:::
|
||
|
||
Tap "OK" in the `exportFile` popup.
|
||
|
||
34) Switch to the Files app and navigate to the Downloads folder.
|
||
|
||
:::note pass
|
||
|
||
When testing `expo-file-system`, select "Documents".
|
||
|
||
:::
|
||
|
||
There will be a new file `sheetjsw.xlsx`.
|
||
|
||
35) Close and reopen the "SheetJSRN" app. The data will reset.
|
||
|
||
36) Tap "Import data" and tap `sheetjsw.xlsx`.
|
||
|
||
If the file is not displayed, tap the `≡` icon and tap "Downloads".
|
||
|
||
:::note pass
|
||
|
||
When testing `expo-file-system`, select "Documents".
|
||
|
||
:::
|
||
|
||
The table will refresh with the data from the exported file.
|
||
|
||
**iOS Device Testing**
|
||
|
||
37) Close any Android / iOS emulators.
|
||
|
||
38) Enable file sharing and make the documents folder visible in the iOS app.
|
||
Add the following lines to `ios/SheetJSRN/Info.plist`:
|
||
|
||
```xml title="ios/SheetJSRN/Info.plist (add to file)"
|
||
<plist version="1.0">
|
||
<dict>
|
||
<!-- highlight-start -->
|
||
<key>UIFileSharingEnabled</key>
|
||
<true/>
|
||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||
<true/>
|
||
<!-- highlight-end -->
|
||
<key>CFBundleDevelopmentRegion</key>
|
||
```
|
||
|
||
(The root element of the document is `plist` and it contains one `dict` child)
|
||
|
||
39) Enable developer code signing certificates. More details are covered in the
|
||
"iOS Device Testing" part of the [Fetch Demo](#fetch-demo) (step 15).
|
||
|
||
40) Install `ios-deploy` through Homebrew:
|
||
|
||
```bash
|
||
brew install ios-deploy
|
||
```
|
||
|
||
41) Run on device:
|
||
|
||
```bash
|
||
npx react-native run-ios
|
||
```
|
||
|
||
If the build fails, some troubleshooting notes are included in the "iOS Device
|
||
Testing" part of the [Fetch Demo](#fetch-demo) (step 17).
|
||
|
||
41) Download https://sheetjs.com/pres.numbers on the device.
|
||
|
||
42) Switch back to the "SheetJSRN" app.
|
||
|
||
43) Tap "Import data" and tap `pres` from the Recents list.
|
||
|
||
The table will refresh with data from the file.
|
||
|
||
44) Tap "Export Data" and tap "OK" in the `exportFile` popup.
|
||
|
||
45) Install the "Numbers" app from the iOS App Store.
|
||
|
||
46) Open the "Files" app. Repeatedly tap the `<` button in the top-left corner
|
||
to return to the "Browse" view.
|
||
|
||
47) Tap "On My iPhone" or "On My iPad". Tap "SheetJSRN" in the list.
|
||
|
||
The `sheetjsw` entry in this folder is the generated file.
|
||
|
||
48) Hold down the `sheetjsw` item until the menu appears. Select "Share".
|
||
|
||
49) In the sharing menu, below a list of contacts, there will be a row of app icons.
|
||
Swipe left until the "Numbers" app icon appears and tap the app icon.
|
||
|
||
The Numbers app will load the spreadsheet, confirming that the file is valid.
|
||
|
||
[^1]: Follow the ["React Native CLI Quickstart"](https://reactnative.dev/docs/environment-setup) and select the appropriate "Development OS".
|
||
[^2]: See ["Array of Arrays" in the API reference](/docs/api/utilities/array#array-of-arrays)
|
||
[^3]: See ["Array Output" in "Utility Functions"](/docs/api/utilities/array#array-output)
|
||
[^4]: See ["Array of Arrays Input" in "Utility Functions"](/docs/api/utilities/array#array-of-arrays-input)
|
||
[^5]: React-Native commit [`5b597b5`](https://github.com/facebook/react-native/commit/5b597b5ff94953accc635ed3090186baeecb3873) added the final piece required for `fetch` support. It is available in official releases starting from `0.72.0`.
|
||
[^6]: When the demo was last tested, the Temurin distribution of Java 17 was installed through the macOS Brew package manager by running `brew install temurin17`. [Direct downloads are available at `adoptium.net`](https://adoptium.net/temurin/releases/?version=17)
|
||
[^7]: See ["Running On Device"](https://reactnative.dev/docs/running-on-device) in the React Native documentation
|
||
[^8]: See [`UIFileSharingEnabled`](https://developer.apple.com/documentation/bundleresources/information_property_list/uifilesharingenabled) in the Apple Developer Documentation.
|
||
[^9]: See [`LSSupportsOpeningDocumentsInPlace`](https://developer.apple.com/documentation/bundleresources/information_property_list/lssupportsopeningdocumentsinplace) in the Apple Developer Documentation.
|
||
[^10]: Follow the ["React Native CLI Quickstart"](https://reactnative.dev/docs/environment-setup) for Android (and iOS, if applicable)
|
||
[^11]: See the [JDK Archive](https://jdk.java.net/archive/) for Java 17 JDK download links. |