655 lines
19 KiB
Markdown
655 lines
19 KiB
Markdown
---
|
|
title: Native Sheets in NativeScript
|
|
sidebar_label: NativeScript
|
|
pagination_prev: demos/static/index
|
|
pagination_next: demos/desktop/index
|
|
sidebar_position: 2
|
|
sidebar_custom_props:
|
|
summary: JS + Native Elements
|
|
---
|
|
|
|
import current from '/version.js';
|
|
import CodeBlock from '@theme/CodeBlock';
|
|
|
|
export const g = {style: {color:"green"}};
|
|
export const r = {style: {color:"red"}};
|
|
export const y = {style: {color:"yellow"}};
|
|
|
|
[NativeScript](https://nativescript.org/) 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 NativeScript and SheetJS to process and generate spreadsheets.
|
|
We'll explore how to load SheetJS in a NativeScript app; parse and generate
|
|
spreadsheets stored on the device; and fetch and parse remote files.
|
|
|
|
The "Complete Example" creates an app that looks like the screenshots below:
|
|
|
|
<table><thead><tr>
|
|
<th><a href="#complete-example">iOS</a></th>
|
|
<th><a href="#complete-example">Android</a></th>
|
|
</tr></thead><tbody><tr><td>
|
|
|
|
![iOS screenshot](pathname:///nativescript/ios.png)
|
|
|
|
</td><td>
|
|
|
|
![Android screenshot](pathname:///nativescript/and.png)
|
|
|
|
</td></tr></tbody></table>
|
|
|
|
:::info pass
|
|
|
|
The discussion covers the NativeScript + Angular integration. Familiarity with
|
|
Angular and TypeScript is assumed.
|
|
|
|
:::
|
|
|
|
:::note Tested Deployments
|
|
|
|
This demo was tested in the following environments:
|
|
|
|
| OS | Type | Device | NS | Date |
|
|
|:-----------|:-----|:--------------------|:---------|:-----------|
|
|
| Android 34 | Sim | Pixel 3a | `8.6.1` | 2023-12-04 |
|
|
| iOS 17.0.1 | Sim | iPhone SE (3rd gen) | `8.6.1` | 2023-12-04 |
|
|
| Android 29 | Real | NVIDIA Shield | `8.6.1` | 2023-12-04 |
|
|
| iOS 15.1 | Real | iPad Pro | `8.6.1` | 2023-12-04 |
|
|
|
|
:::
|
|
|
|
:::warning Telemetry
|
|
|
|
Before starting this demo, manually disable telemetry.
|
|
|
|
NativeScript 8.6.1 split the telemetry into two parts: "usage" and "error". Both
|
|
must be disabled separately:
|
|
|
|
```bash
|
|
npx -p nativescript ns usage-reporting disable
|
|
npx -p nativescript ns error-reporting disable
|
|
```
|
|
|
|
To verify telemetry was disabled:
|
|
|
|
```bash
|
|
npx -p nativescript ns usage-reporting status
|
|
npx -p nativescript ns error-reporting status
|
|
```
|
|
|
|
:::
|
|
|
|
## Integration Details
|
|
|
|
The [SheetJS NodeJS Module](/docs/getting-started/installation/nodejs) can be
|
|
imported from any component or script in the app.
|
|
|
|
The `@nativescript/core/file-system` package provides classes for file access.
|
|
The `File` class does not support binary data, but the file access singleton
|
|
from `@nativescript/core` does support reading and writing `ArrayBuffer`.
|
|
|
|
Reading and writing data require a URL. The following snippet searches typical
|
|
document folders for a specified filename:
|
|
|
|
```ts
|
|
import { Folder, knownFolders, path } from '@nativescript/core/file-system';
|
|
|
|
function get_url_for_filename(filename: string): string {
|
|
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
|
|
return path.normalize(target.path + "///" + filename);
|
|
}
|
|
```
|
|
|
|
### Reading Local Files
|
|
|
|
`getFileAccess().readBufferAsync` can read data into an `ArrayBuffer` object.
|
|
The SheetJS `read` method[^1] can parse this data into a workbook object.[^2]
|
|
|
|
```ts
|
|
import { getFileAccess } from '@nativescript/core';
|
|
import { read } from 'xlsx';
|
|
|
|
/* find appropriate path */
|
|
const url = get_url_for_filename("SheetJSNS.xls");
|
|
|
|
/* get data */
|
|
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(url);
|
|
|
|
/* read workbook */
|
|
const wb = read(ab);
|
|
```
|
|
|
|
After parsing into a workbook, the `sheet_to_json`[^3] method can generate row
|
|
data objects:
|
|
|
|
```ts
|
|
import { utils } from 'xlsx';
|
|
|
|
/* grab first sheet */
|
|
const wsname: string = wb.SheetNames[0];
|
|
const ws = wb.Sheets[wsname];
|
|
|
|
/* generate array of row objects */
|
|
const data = utils.sheet_to_json(ws);
|
|
```
|
|
|
|
### Writing Local Files
|
|
|
|
The SheetJS `write` method[^4] with the option `type: "binary"` will generate
|
|
`Uint8Array` objects. `getFileAccess().writeBufferAsync` can write data from a
|
|
`Uint8Array` object to the device.
|
|
|
|
iOS supports `Uint8Array` directly but Android requires a true array of numbers:
|
|
|
|
```ts
|
|
import { getFileAccess } from '@nativescript/core';
|
|
import { write } from 'xlsx';
|
|
|
|
/* find appropriate path */
|
|
const url = get_url_for_filename("SheetJSNS.xls");
|
|
|
|
/* generate Uint8Array */
|
|
const u8: Uint8Array = write(wb, { bookType: 'xls', type: 'binary' });
|
|
|
|
/* attempt to save Uint8Array to file */
|
|
await getFileAccess().writeBufferAsync(url, global.isAndroid ? (Array.from(u8) as any) : u8);
|
|
```
|
|
|
|
A worksheet can be generated from an array of row objects with the SheetJS
|
|
`json_to_sheet` method[^5]. After generating an array, the `book_new` and
|
|
`book_append_sheet` methods[^6] can create the workbook.
|
|
|
|
### Fetching Remote Files
|
|
|
|
`getFile` from `@nativescript/core/http` can download files. After storing the
|
|
file in a temporary folder, `getFileAccess().readBufferAsync` can read the data
|
|
and the SheetJS `read` method[^7] can parse the file:
|
|
|
|
```ts
|
|
import { knownFolders, path, getFileAccess } from '@nativescript/core'
|
|
import { getFile } from '@nativescript/core/http';
|
|
import { read } from 'xlsx';
|
|
|
|
/* generate temporary path for the new file */
|
|
const temp: string = path.join(knownFolders.temp().path, "pres.xlsx");
|
|
|
|
/* download file */
|
|
const file = await getFile("https://sheetjs.com/pres.xlsx", temp)
|
|
|
|
/* get data */
|
|
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(file.path);
|
|
|
|
/* read workbook */
|
|
const wb = read(ab);
|
|
```
|
|
|
|
## Complete Example
|
|
|
|
### Platform Configuration
|
|
|
|
0) Disable telemetry:
|
|
|
|
```bash
|
|
npx -p nativescript ns usage-reporting disable
|
|
npx -p nativescript ns error-reporting disable
|
|
```
|
|
|
|
1) Follow the official Environment Setup instructions[^8].
|
|
|
|
:::caution pass
|
|
|
|
When the demo was last tested, the latest version of the Android API was 34.
|
|
NativeScript did not support that API level. The exact error message from
|
|
`npx -p nativescript ns doctor ios` clearly stated supported versions:
|
|
|
|
(x is red, body text is yellow)
|
|
```
|
|
✖ No compatible version of the Android SDK Build-tools are installed on your system. You can install any version in the following range: '>=23 <=33'.
|
|
```
|
|
|
|
The SDK Platform `Android 13.0 ("Tiramisu")` was compatible with NativeScript.
|
|
Until NativeScript properly supports API level 34, "Tiramisu" must be used.
|
|
This requires installing the following packages from Android Studio:
|
|
|
|
- `Android 13.0 ("Tiramisu")` API Level `33`
|
|
- `Android SDK Build-Tools` Version `33.0.2`
|
|
|
|
:::
|
|
|
|
2) Test the local system configuration for Android development:
|
|
|
|
```bash
|
|
npx -p nativescript ns doctor android
|
|
```
|
|
|
|
In the last macOS test, the following output was displayed:
|
|
|
|
<details open><summary><b>Expected output</b> (click to hide)</summary>
|
|
|
|
<pre>
|
|
<span {...g}>✔</span> Getting environment information{'\n'}
|
|
{'\n'}
|
|
<b>No issues were detected.</b>{'\n'}
|
|
<span {...g}>✔</span> Your ANDROID_HOME environment variable is set and points to correct directory.{'\n'}
|
|
<span {...g}>✔</span> Your adb from the Android SDK is correctly installed.{'\n'}
|
|
<span {...g}>✔</span> The Android SDK is installed.{'\n'}
|
|
<span {...g}>✔</span> A compatible Android SDK for compilation is found.{'\n'}
|
|
<span {...g}>✔</span> Javac is installed and is configured properly.{'\n'}
|
|
<span {...g}>✔</span> The Java Development Kit (JDK) is installed and is configured properly.{'\n'}
|
|
<span {...g}>✔</span> Getting NativeScript components versions information...{'\n'}
|
|
<span {...g}>✔</span> Component nativescript has 8.6.1 version and is up to date.
|
|
</pre>
|
|
|
|
</details>
|
|
|
|
3) Test the local system configuration for iOS development (macOS only):
|
|
|
|
```bash
|
|
npx -p nativescript ns doctor ios
|
|
```
|
|
|
|
In the last macOS test, the following output was displayed:
|
|
|
|
<details open><summary><b>Expected output</b> (click to hide)</summary>
|
|
|
|
<pre>
|
|
<span {...g}>✔</span> Getting environment information{'\n'}
|
|
{'\n'}
|
|
No issues were detected.{'\n'}
|
|
<span {...g}>✔</span> Xcode is installed and is configured properly.{'\n'}
|
|
<span {...g}>✔</span> xcodeproj is installed and is configured properly.{'\n'}
|
|
<span {...g}>✔</span> CocoaPods are installed.{'\n'}
|
|
<span {...g}>✔</span> CocoaPods update is not required.{'\n'}
|
|
<span {...g}>✔</span> CocoaPods are configured properly.{'\n'}
|
|
<span {...g}>✔</span> Your current CocoaPods version is newer than 1.0.0.{'\n'}
|
|
<span {...g}>✔</span> Python installed and configured correctly.{'\n'}
|
|
<span {...g}>✔</span> The Python 'six' package is found.{'\n'}
|
|
<span {...g}>✔</span> Xcode version 15.0.1 satisfies minimum required version 10.{'\n'}
|
|
<span {...g}>✔</span> Getting NativeScript components versions information...{'\n'}
|
|
<span {...g}>✔</span> Component nativescript has 8.6.1 version and is up to date.
|
|
</pre>
|
|
|
|
</details>
|
|
|
|
### Base Project
|
|
|
|
4) Create a skeleton NativeScript + Angular app:
|
|
|
|
```bash
|
|
npx -p nativescript ns create SheetJSNS --ng
|
|
```
|
|
|
|
5) Launch the app in the android simulator to verify the app:
|
|
|
|
```bash
|
|
cd SheetJSNS
|
|
npx -p nativescript ns run android
|
|
```
|
|
|
|
(this may take a while)
|
|
|
|
Once the simulator launches and the test app is displayed, end the script by
|
|
selecting the terminal and entering the key sequence `CTRL + C`
|
|
|
|
:::note pass
|
|
|
|
If the emulator is not running, `nativescript` may fail with the message:
|
|
|
|
```
|
|
Emulator start failed with: No emulator image available for device identifier 'undefined'.
|
|
```
|
|
|
|
:::
|
|
|
|
6) From the project folder, install the library:
|
|
|
|
<CodeBlock language="bash">{`\
|
|
npm i --save https://cdn.sheetjs.com/xlsx-${current}/xlsx-${current}.tgz`}
|
|
</CodeBlock>
|
|
|
|
### Add SheetJS
|
|
|
|
:::note pass
|
|
|
|
The goal of this section is to display the SheetJS library version number.
|
|
|
|
:::
|
|
|
|
7) Edit `src/app/item/items.component.ts` so that the component imports the
|
|
SheetJS version string and adds it to a `version` variable in the component:
|
|
|
|
```ts title="src/app/item/items.component.ts"
|
|
// highlight-next-line
|
|
import { version } from 'xlsx';
|
|
import { Component, OnInit } from '@angular/core'
|
|
|
|
// ...
|
|
|
|
export class ItemsComponent implements OnInit {
|
|
items: Array<Item>
|
|
// highlight-next-line
|
|
version = `SheetJS - ${version}`;
|
|
|
|
constructor(private itemService: ItemService) {}
|
|
// ...
|
|
```
|
|
|
|
8) Edit the template `src/app/item/items.component.html` to reference `version`
|
|
in the title of the action bar:
|
|
|
|
```xml title="src/app/item/items.component.html"
|
|
<!-- highlight-next-line -->
|
|
<ActionBar [title]="version"></ActionBar>
|
|
|
|
<GridLayout>
|
|
<!-- ... -->
|
|
```
|
|
|
|
9) Relaunch the app in the Android simulator:
|
|
|
|
```bash
|
|
npx -p nativescript ns run android
|
|
```
|
|
|
|
The title bar should show the version.
|
|
|
|
![NativeScript Step 4](pathname:///nativescript/step4.png)
|
|
|
|
### Local Files
|
|
|
|
10) Add the Import and Export buttons to the template:
|
|
|
|
```xml title="src/app/item/items.component.html"
|
|
<ActionBar [title]="version"></ActionBar>
|
|
|
|
<!-- highlight-start -->
|
|
<StackLayout>
|
|
<StackLayout orientation="horizontal">
|
|
<Button text="Import File" (tap)="import()" style="padding: 10px"></Button>
|
|
<Button text="Export File" (tap)="export()" style="padding: 10px"></Button>
|
|
</StackLayout>
|
|
<!-- highlight-end -->
|
|
<ListView [items]="items">
|
|
<!-- ... -->
|
|
</ListView>
|
|
<!-- highlight-next-line -->
|
|
</StackLayout>
|
|
```
|
|
|
|
11) Add the `import` and `export` methods in the component script:
|
|
|
|
```ts title="src/app/item/items.component.ts"
|
|
// highlight-start
|
|
import { version, utils, read, write } from 'xlsx';
|
|
import { Dialogs, getFileAccess } from '@nativescript/core';
|
|
import { Folder, knownFolders, path } from '@nativescript/core/file-system';
|
|
// highlight-end
|
|
import { Component, OnInit } from '@angular/core'
|
|
|
|
import { Item } from './item'
|
|
import { ItemService } from './item.service'
|
|
|
|
// highlight-start
|
|
function get_url_for_filename(filename: string): string {
|
|
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
|
|
return path.normalize(target.path + "///" + filename);
|
|
}
|
|
// highlight-end
|
|
|
|
@Component({
|
|
selector: 'ns-items',
|
|
templateUrl: './items.component.html',
|
|
})
|
|
export class ItemsComponent implements OnInit {
|
|
items: Array<Item>
|
|
version: string = `SheetJS - ${version}`;
|
|
|
|
constructor(private itemService: ItemService) {}
|
|
|
|
ngOnInit(): void {
|
|
this.items = this.itemService.getItems()
|
|
}
|
|
|
|
// highlight-start
|
|
/* Import button */
|
|
async import() {
|
|
}
|
|
|
|
/* Export button */
|
|
async export() {
|
|
}
|
|
// highlight-end
|
|
}
|
|
```
|
|
|
|
12) Restart the app process. Two buttons should show up at the top:
|
|
|
|
![NativeScript Step 5](pathname:///nativescript/step5.png)
|
|
|
|
13) Implement import and export by adding the highlighted lines:
|
|
|
|
```ts title="src/app/item/items.component.ts"
|
|
/* Import button */
|
|
async import() {
|
|
// highlight-start
|
|
/* find appropriate path */
|
|
const url = get_url_for_filename("SheetJSNS.xls");
|
|
|
|
try {
|
|
await Dialogs.alert(`Attempting to read from SheetJSNS.xls at ${url}`);
|
|
/* get data */
|
|
const ab: ArrayBuffer = await getFileAccess().readBufferAsync(url);
|
|
|
|
/* read workbook */
|
|
const wb = read(ab);
|
|
|
|
/* grab first sheet */
|
|
const wsname: string = wb.SheetNames[0];
|
|
const ws = wb.Sheets[wsname];
|
|
|
|
/* update table */
|
|
this.items = utils.sheet_to_json<Item>(ws);
|
|
} catch(e) { await Dialogs.alert(e.message); }
|
|
// highlight-end
|
|
}
|
|
|
|
/* Export button */
|
|
async export() {
|
|
// highlight-start
|
|
/* find appropriate path */
|
|
const url = get_url_for_filename("SheetJSNS.xls");
|
|
|
|
try {
|
|
/* create worksheet from data */
|
|
const ws = utils.json_to_sheet(this.items);
|
|
|
|
/* create workbook from worksheet */
|
|
const wb = utils.book_new();
|
|
utils.book_append_sheet(wb, ws, "Sheet1");
|
|
|
|
/* generate Uint8Array */
|
|
const u8: Uint8Array = write(wb, { bookType: 'xls', type: 'buffer' });
|
|
|
|
/* attempt to save Uint8Array to file */
|
|
await getFileAccess().writeBufferAsync(url, global.isAndroid ? (Array.from(u8) as any) : u8);
|
|
await Dialogs.alert(`Wrote to SheetJSNS.xls at ${url}`);
|
|
} catch(e) { await Dialogs.alert(e.message); }
|
|
// highlight-end
|
|
}
|
|
```
|
|
|
|
### Android
|
|
|
|
14) Launch the app in the Android Simulator:
|
|
|
|
```bash
|
|
npx -p nativescript ns run android
|
|
````
|
|
|
|
If the app does not automatically launch, manually open the `SheetJSNS` app.
|
|
|
|
15) Tap "Export File". A dialog will print where the file was written. Typically
|
|
the URL is `/data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls`
|
|
|
|
16) Pull the file from the simulator:
|
|
|
|
```bash
|
|
adb root
|
|
adb pull /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls SheetJSNS.xls
|
|
```
|
|
|
|
If the emulator cannot be rooted:
|
|
|
|
```bash
|
|
adb shell "run-as org.nativescript.SheetJSNS cat /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls" > SheetJSNS.xls
|
|
```
|
|
|
|
17) Open `SheetJSNS.xls` with a spreadsheet editor.
|
|
|
|
After the header row, insert a row with cell A2 = 0, B2 = SheetJS, C2 = Library:
|
|
|
|
```
|
|
id | name | role
|
|
0 | SheetJS | Library
|
|
1 | Ter Stegen | Goalkeeper
|
|
3 | Piqué | Defender
|
|
...
|
|
```
|
|
|
|
18) Push the file back to the simulator:
|
|
|
|
```bash
|
|
adb push SheetJSNS.xls /data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls
|
|
```
|
|
|
|
If the emulator cannot be rooted:
|
|
|
|
```bash
|
|
dd if=SheetJSNS.xls | adb shell "run-as org.nativescript.SheetJSNS dd of=/data/user/0/org.nativescript.SheetJSNS/files/SheetJSNS.xls"
|
|
```
|
|
|
|
19) Tap "Import File". A dialog will print the path of the file that was read.
|
|
The first item in the list will change.
|
|
|
|
### iOS
|
|
|
|
20) Launch the app in the iOS Simulator:
|
|
|
|
```bash
|
|
npx -p nativescript ns run ios
|
|
```
|
|
|
|
21) Tap "Export File". A dialog will print where the file was written.
|
|
|
|
22) Open the file with a spreadsheet editor.
|
|
|
|
After the header row, insert a row with cell A2 = 0, B2 = SheetJS, C2 = Library:
|
|
|
|
```
|
|
id | name | role
|
|
0 | SheetJS | Library
|
|
1 | Ter Stegen | Goalkeeper
|
|
3 | Piqué | Defender
|
|
...
|
|
```
|
|
|
|
23) Restart the app after saving the file.
|
|
|
|
24) Tap "Import File". A dialog will print the path of the file that was read.
|
|
The first item in the list will change:
|
|
|
|
![NativeScript Step 7](pathname:///nativescript/step7.png)
|
|
|
|
### Fetching Files
|
|
|
|
25) In `src/app/item/items.component.ts`, make `ngOnInit` asynchronous:
|
|
|
|
```ts title="src/app/item/items.component.ts"
|
|
async ngOnInit(): Promise<void> {
|
|
this.items = await this.itemService.getItems()
|
|
}
|
|
```
|
|
|
|
26) Replace `item.service.ts` with the following:
|
|
|
|
```ts title="src/app/item/item.service.ts"
|
|
import { Injectable } from '@angular/core'
|
|
|
|
import { knownFolders, path, getFileAccess } from '@nativescript/core'
|
|
import { getFile } from '@nativescript/core/http';
|
|
import { read, utils } from 'xlsx';
|
|
|
|
import { Item } from './item'
|
|
interface IPresident { Name: string; Index: number };
|
|
|
|
@Injectable({ providedIn: 'root' })
|
|
export class ItemService {
|
|
private items: Array<Item>;
|
|
|
|
async getItems(): Promise<Array<Item>> {
|
|
/* fetch https://sheetjs.com/pres.xlsx */
|
|
const temp: string = path.join(knownFolders.temp().path, "pres.xlsx");
|
|
const ab = await getFile("https://sheetjs.com/pres.xlsx", temp)
|
|
/* read the temporary file */
|
|
const wb = read(await getFileAccess().readBufferAsync(ab.path));
|
|
/* translate the first worksheet to the required Item type */
|
|
const data = utils.sheet_to_json<IPresident>(wb.Sheets[wb.SheetNames[0]]);
|
|
return this.items = data.map((pres, id) => ({id, name: pres.Name, role: ""+pres.Index} as Item));
|
|
}
|
|
|
|
getItem(id: number): Item {
|
|
return this.items.filter((item) => item.id === id)[0]
|
|
}
|
|
}
|
|
```
|
|
|
|
27) Relaunch the app in the Android simulator:
|
|
|
|
```bash
|
|
npx -p nativescript ns run android
|
|
````
|
|
|
|
The app should show Presidential data.
|
|
|
|
### Android Device
|
|
|
|
28) Connect an Android device using a USB cable.
|
|
|
|
If the device asks to allow USB debugging, tap "Allow".
|
|
|
|
29) Close any Android / iOS emulators.
|
|
|
|
30) Build APK and run on device:
|
|
|
|
```bash
|
|
npx -p nativescript ns run android
|
|
```
|
|
|
|
If the Android emulators are closed and an Android device is connected, the last
|
|
command will build an APK and install on the device.
|
|
|
|
### iOS Device
|
|
|
|
31) Connect an iOS device using a USB cable
|
|
|
|
32) Close any Android / iOS emulators.
|
|
|
|
33) Enable developer code signing certificates[^9]
|
|
|
|
34) Run on device:
|
|
|
|
```bash
|
|
npx -p nativescript ns run ios
|
|
```
|
|
|
|
[^1]: See [`read` in "Reading Files"](/docs/api/parse-options)
|
|
[^2]: See ["Workbook Object"](/docs/csf/book)
|
|
[^3]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
|
|
[^4]: See [`write` in "Writing Files"](/docs/api/write-options)
|
|
[^5]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
|
|
[^6]: See ["Workbook Helpers" in "Utilities"](/docs/api/utilities/wb) for details on `book_new` and `book_append_sheet`.
|
|
[^7]: See [`read` in "Reading Files"](/docs/api/parse-options)
|
|
[^8]: See ["Local setup"](https://docs.nativescript.org/setup/#local-setup) in the NativeScript documentation. For Windows and Linux, follow the "Android" instructions. For macOS, follow both the iOS and Android instructions.
|
|
[^9]: The [Flutter documentation](https://docs.flutter.dev/get-started/install/macos?tab=ios15#enable-developer-code-signing-certificates) covers the instructions in more detail. The correct workspace is `platforms/ios/SheetJSNS.xcodeproj/project.xcworkspace` |