forked from sheetjs/docs.sheetjs.com
378 lines
10 KiB
Markdown
378 lines
10 KiB
Markdown
|
---
|
||
|
sidebar_position: 19
|
||
|
title: iOS and Android Apps
|
||
|
---
|
||
|
|
||
|
Many mobile app frameworks mix JavaScript / CSS / HTML5 concepts with native
|
||
|
extensions and libraries to create a hybrid development experience. Developers
|
||
|
well-versed in web technologies can now build actual mobile applications that
|
||
|
run on iOS and Android!
|
||
|
|
||
|
:::warning
|
||
|
|
||
|
**The ecosystem has broken backwards-compatibility many times!**
|
||
|
|
||
|
iOS and Android, as well as the underlying JavaScript frameworks, make breaking
|
||
|
changes regularly. The demos were tested against emulators / real devices at
|
||
|
some point in time. A framework or OS change can render the demos inoperable.
|
||
|
|
||
|
Each demo section will mention test dates and platform versions.
|
||
|
|
||
|
:::
|
||
|
|
||
|
The ["JavaScript Engines"](./engines) section includes samples for JavaScript
|
||
|
engines used in the mobile app frameworks. SheetJS libraries have been tested
|
||
|
in the relevant engines and should "just work" with some caveats.
|
||
|
|
||
|
:::caution readFile and writeFile
|
||
|
|
||
|
`XLSX.readFile` and `XLSX.writeFile` do not work in mobile apps! The demos
|
||
|
include platform-specific details for fetching file data for `XLSX.read` and
|
||
|
writing file data generated by `XLSX.write`.
|
||
|
|
||
|
Some platforms provide this functionality as part of the standard library.
|
||
|
Other platforms, including React Native, do not. When the platform does not
|
||
|
provide, usually there are third-party modules to provide needed functionality.
|
||
|
|
||
|
:::
|
||
|
|
||
|
## NativeScript
|
||
|
|
||
|
:::note
|
||
|
|
||
|
This demo was tested on an Intel Mac on August 10 2022. NativeScript version
|
||
|
(as verified with `ns --version`) is `8.3.2`. The iOS simulator runs iOS 15.5
|
||
|
on an iPhone SE 3rd generation.
|
||
|
|
||
|
:::
|
||
|
|
||
|
:::warning Binary Data issues
|
||
|
|
||
|
NativeScript will not safely transmit binary or UTF8 strings. XLSB, NUMBERS,
|
||
|
XLSX, XLS, ODS, SYLK, and DBF exports are known to be mangled.
|
||
|
|
||
|
[This is a known NativeScript bug](https://github.com/NativeScript/NativeScript/issues/9586)
|
||
|
|
||
|
This demo will focus on ASCII CSV files. Once the bug is resolved, XLSX and
|
||
|
other formats will be supported.
|
||
|
|
||
|
:::
|
||
|
|
||
|
The `@nativescript/core/file-system` package provides classes for file access.
|
||
|
|
||
|
### Integration Details
|
||
|
|
||
|
Reading and writing data require a file handle. The following snippet searches
|
||
|
typical document folders for a specified filename:
|
||
|
|
||
|
```ts
|
||
|
import { File, Folder, knownFolders, path } from '@nativescript/core/file-system';
|
||
|
|
||
|
function get_handle_for_filename(filename: string): File {
|
||
|
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
|
||
|
const url: string = path.normalize(target.path + "///" + filename);
|
||
|
return File.fromPath(url);
|
||
|
}
|
||
|
```
|
||
|
|
||
|
The encoding `ISO_8859_1` spiritually resembles the `"binary"` SheetJS type
|
||
|
|
||
|
**Reading data**
|
||
|
|
||
|
`File#readText(encoding.ISO_8859_1)` returns strings compatible with `"binary"`
|
||
|
|
||
|
```ts
|
||
|
/* get binary string */
|
||
|
const bstr: string = await file.readText(encoding.ISO_8859_1);
|
||
|
|
||
|
/* read workbook */
|
||
|
const wb = read(bstr, { type: "binary" });
|
||
|
```
|
||
|
|
||
|
**Writing data**
|
||
|
|
||
|
`File#writeText` with the `ISO_8859_1` encoding accepts `"binary"` strings with
|
||
|
the caveat listed in the warning at the top of this section:
|
||
|
|
||
|
```ts
|
||
|
/* generate binary string */
|
||
|
const bstr: string = write(wb, { bookType: 'csv', type: 'binary' });
|
||
|
|
||
|
/* attempt to save binary string to file */
|
||
|
await file.writeText(bstr, encoding.ISO_8859_1);
|
||
|
```
|
||
|
|
||
|
### Demo
|
||
|
|
||
|
The demo builds off of the NativeScript + Angular example. Familiarity with
|
||
|
with Angular and TypeScript is assumed.
|
||
|
|
||
|
<details open><summary><b>Complete Example</b> (click to show)</summary>
|
||
|
|
||
|
0) Follow the official Environment Setup instructions (tested with "macOS + iOS")
|
||
|
|
||
|
1) Create a skeleton NativeScript + Angular app:
|
||
|
|
||
|
```bash
|
||
|
ns create SheetJSNS --ng
|
||
|
```
|
||
|
|
||
|
2) Launch the app in the iOS simulator to verify that the demo built properly:
|
||
|
|
||
|
```bash
|
||
|
cd SheetJSNS
|
||
|
ns run ios
|
||
|
```
|
||
|
|
||
|
(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`
|
||
|
|
||
|
3) From the project folder, install the library:
|
||
|
|
||
|
```bash
|
||
|
npm install --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz
|
||
|
```
|
||
|
|
||
|
4) To confirm the library was loaded, change the title to show the version. The
|
||
|
differences are highlighted.
|
||
|
|
||
|
`src/app/item/items.component.ts` imports the version string to the component:
|
||
|
|
||
|
```ts title="src/app/item/items.component.ts"
|
||
|
// highlight-next-line
|
||
|
import { version } from 'xlsx';
|
||
|
import { Component, OnInit } from '@angular/core'
|
||
|
|
||
|
import { Item } from './item'
|
||
|
import { ItemService } from './item.service'
|
||
|
|
||
|
@Component({
|
||
|
selector: 'ns-items',
|
||
|
templateUrl: './items.component.html',
|
||
|
})
|
||
|
export class ItemsComponent implements OnInit {
|
||
|
items: Array<Item>
|
||
|
// highlight-next-line
|
||
|
version = `SheetJS - ${version}`;
|
||
|
|
||
|
constructor(private itemService: ItemService) {}
|
||
|
|
||
|
ngOnInit(): void {
|
||
|
this.items = this.itemService.getItems()
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
`src/app/item/items.component.html` references the version in the title:
|
||
|
|
||
|
```xml title="src/app/item/items.component.html"
|
||
|
<!-- highlight-next-line -->
|
||
|
<ActionBar [title]="version"></ActionBar>
|
||
|
|
||
|
<GridLayout>
|
||
|
<ListView [items]="items">
|
||
|
<ng-template let-item="item">
|
||
|
<StackLayout [nsRouterLink]="['/item', item.id]">
|
||
|
<Label [text]="item.name"></Label>
|
||
|
</StackLayout>
|
||
|
</ng-template>
|
||
|
</ListView>
|
||
|
</GridLayout>
|
||
|
```
|
||
|
|
||
|
Relaunch the app with `ns run ios` and the title bar should show the version.
|
||
|
|
||
|
![NativeScript Step 4](pathname:///mobile/nativescript4.png)
|
||
|
|
||
|
5) 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">
|
||
|
<ng-template let-item="item">
|
||
|
<StackLayout [nsRouterLink]="['/item', item.id]">
|
||
|
<Label [text]="item.name"></Label>
|
||
|
</StackLayout>
|
||
|
</ng-template>
|
||
|
</ListView>
|
||
|
<!-- highlight-next-line -->
|
||
|
</StackLayout>
|
||
|
```
|
||
|
|
||
|
```ts title="src/app/item/items.component.ts"
|
||
|
// highlight-start
|
||
|
import { version, utils, read, write } from 'xlsx';
|
||
|
import { Dialogs } from '@nativescript/core';
|
||
|
import { encoding } from '@nativescript/core/text';
|
||
|
import { File, 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_handle_for_filename(filename: string): [File, string] {
|
||
|
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
|
||
|
const url: string = path.normalize(target.path + "///" + filename);
|
||
|
return [File.fromPath(url), url];
|
||
|
}
|
||
|
// 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
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Restart the app process and two buttons should show up at the top:
|
||
|
|
||
|
![NativeScript Step 5](pathname:///mobile/nativescript5.png)
|
||
|
|
||
|
6) Implement import and export:
|
||
|
|
||
|
```ts title="src/app/item/items.component.ts"
|
||
|
import { version, utils, read, write } from 'xlsx';
|
||
|
import { Dialogs } from '@nativescript/core';
|
||
|
import { encoding } from '@nativescript/core/text';
|
||
|
import { File, Folder, knownFolders, path } from '@nativescript/core/file-system';
|
||
|
import { Component, OnInit } from '@angular/core'
|
||
|
|
||
|
import { Item } from './item'
|
||
|
import { ItemService } from './item.service'
|
||
|
|
||
|
function get_handle_for_filename(filename: string): [File, string] {
|
||
|
const target: Folder = knownFolders.documents() || knownFolders.ios.sharedPublic();
|
||
|
const url: string = path.normalize(target.path + "///" + filename);
|
||
|
return [File.fromPath(url), url];
|
||
|
}
|
||
|
|
||
|
@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()
|
||
|
}
|
||
|
|
||
|
/* Import button */
|
||
|
async import() {
|
||
|
// highlight-start
|
||
|
/* find appropriate path */
|
||
|
const [file, url] = get_handle_for_filename("SheetJSNS.csv");
|
||
|
|
||
|
try {
|
||
|
/* get binary string */
|
||
|
const bstr: string = await file.readText(encoding.ISO_8859_1);
|
||
|
|
||
|
/* read workbook */
|
||
|
const wb = read(bstr, { type: "binary" });
|
||
|
|
||
|
/* grab first sheet */
|
||
|
const wsname: string = wb.SheetNames[0];
|
||
|
const ws = wb.Sheets[wsname];
|
||
|
|
||
|
/* update table */
|
||
|
this.items = utils.sheet_to_json<Item>(ws);
|
||
|
Dialogs.alert(`Attempting to read to ${filename} in ${url}`);
|
||
|
} catch(e) { Dialogs.alert(e.message); }
|
||
|
// highlight-end
|
||
|
}
|
||
|
|
||
|
/* Export button */
|
||
|
async export() {
|
||
|
// highlight-start
|
||
|
/* find appropriate path */
|
||
|
const [file, url] = get_handle_for_filename("SheetJSNS.csv");
|
||
|
|
||
|
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 binary string */
|
||
|
const wbout: string = write(wb, { bookType: 'csv', type: 'binary' });
|
||
|
|
||
|
/* attempt to save binary string to file */
|
||
|
await file.writeText(wbout, encoding.ISO_8859_1);
|
||
|
Dialogs.alert(`Wrote to ${filename} in ${url}`);
|
||
|
} catch(e) { Dialogs.alert(e.message); }
|
||
|
// highlight-end
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
Restart the app process.
|
||
|
|
||
|
**Testing**
|
||
|
|
||
|
The app can be tested with the following sequence in the simulator:
|
||
|
|
||
|
- Hit "Export File". A dialog will print where the file was written
|
||
|
|
||
|
- Open that file with a text editor. It will be a 3-column CSV:
|
||
|
|
||
|
```csv
|
||
|
id,name,role
|
||
|
1,Ter Stegen,Goalkeeper
|
||
|
3,Piqué,Defender
|
||
|
4,I. Rakitic,Midfielder
|
||
|
...
|
||
|
```
|
||
|
|
||
|
After the header row, add the line `0,SheetJS,Library`:
|
||
|
|
||
|
```csv
|
||
|
id,name,role
|
||
|
0,SheetJS,Library
|
||
|
1,Ter Stegen,Goalkeeper
|
||
|
3,Piqué,Defender
|
||
|
...
|
||
|
```
|
||
|
|
||
|
- Hit "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:///mobile/nativescript7.png)
|
||
|
|
||
|
</details>
|