--- 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 2022 August 10. 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.
Complete Example (click to show) 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 i --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 // 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" ``` 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" ``` ```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 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 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(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)
## Quasar :::note This demo was tested on an Intel Mac on 2022 August 14. Quasar version `2.7.7`. The iOS simulator runs iOS 15.5 on an iPhone SE 3rd generation. ::: This demo will focus on VueJS and Cordova with the Quasar Vite starter project. ### Integration Details The complete solution uses `cordova-plugin-file` for file operations. It can be installed like any other Cordova plugin: ```bash cd src-cordova cordova plugin add cordova-plugin-file cd .. ``` #### Reading data The `q-file` component presents an API reminiscent of File Input elements: ```html ``` When binding to the `input` element, the callback receives an `Event` object: ```ts import { read } from 'xlsx'; // assuming `todos` is a standard VueJS `ref` async function updateFile(v) { try { // `v.target.files[0]` is the desired file object const files = (v.target as HTMLInputElement).files; if(!files || files.length == 0) return; // read first file const wb = read(await files[0].arrayBuffer()); // get data of first worksheet as an array of objects const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); // update state todos.value = data.map(row => ({id: row.Index, content: row.Name})); } catch(e) { console.log(e); } } ``` #### Writing data The API is shaped like the File and Directory Entries API. For clarity, since the code is a "pyramid of doom", the error handlers are omitted: ```ts import { write } from 'xlsx'; // on iOS and android, `XLSX.write` with type "buffer" returns a `Uint8Array` const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"}); // Request filesystem access for persistent storage window.requestFileSystem(window.PERSISTENT, 0, function(fs) { // Request a handle to "SheetJSQuasar.xlsx", making a new file if necessary fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => { // Request a FileWriter for writing data entry.createWriter(writer => { // The FileWriter API needs an actual Blob const data = new Blob([u8], {type: "application/vnd.ms-excel"}); // This callback is called if the write is successful writer.onwriteend = () => { // TODO: show a dialog }; // writer.onerror will be invoked if there is an error in writing // write the data writer.write(data); }); }); }); ``` ### Demo The demo builds off of the Vite example. Familiarity with VueJS and TypeScript is assumed.
Complete Example (click to show) 0) Ensure all of the dependencies are installed. Install the CLI globally: ```bash npm i -g @quasar/cli cordova ``` (you may need to run `sudo npm i -g` if there are permission issues) 1) Create a new app: ```bash npm init quasar ``` When prompted: - "What would you like to build?": `App with Quasar CLI` - "Project folder": `SheetJSQuasar` - "Pick Quasar version": `Quasar v2 (Vue 3 | latest and greatest)` - "Pick script type": `Typescript` - "Pick Quasar App CLI variant": `Quasar App CLI with Vite` - "Package name": (just press enter, it will use the default `sheetjsquasar` - "Project product name": `SheetJSQuasar` - "Project description": `SheetJS + Quasar` - "Author": (just press enter, it will use your git config settings) - "Pick a Vue component style": `Composition API` - "Pick your CSS preprocessor": `None` - "Check the features needed for your project": Deselect everything - "Install project dependencies": `No` 2) Install dependencies: ```bash cd SheetJSQuasar npm i npm i --save https://cdn.sheetjs.com/xlsx-latest/xlsx-latest.tgz ``` 3) Set up Cordova: ```bash quasar mode add cordova ``` When prompted, enter the app id `org.sheetjs.quasar`. It will create a new `src-cordova` folder. Continue in that folder: ```bash cd src-cordova cordova platform add ios cordova plugin add cordova-plugin-wkwebview-engine cordova plugin add cordova-plugin-file ``` :::note If there is an error `Could not load API for iOS project`, it needs to be reset: ```bash cordova platform rm ios cordova platform add ios cordova plugin add cordova-plugin-file ``` ::: Return to the project directory: ```bash cd .. ``` 4) Start the dev server: ```bash quasar dev -m ios ``` :::caution If the app is blank, delete the app and close the simulator, then restart dev ::: ![Quasar Step 4](pathname:///mobile/quasar4.png) 5) Add the Dialog plugin to `quasar.config.js`: ```js title="quasar.config.js" // Quasar plugins // highlight-next-line plugins: ['Dialog'] ``` 6) In the template section of `src/pages/IndexPage.vue`, add a Save button and a Load file picker component at the bottom of the page: ```html title="src/pages/IndexPage.vue" ``` This uses two functions that should be added to the component script: ```ts title="src/pages/IndexPage.vue" const meta = ref({ totalCount: 1200 }); // highlight-start function saveFile() { } async function updateFile(v) { } return { todos, meta, saveFile, updateFile }; // highlight-end } }); ``` The app should now show two buttons at the bottom: ![Quasar Step 6](pathname:///mobile/quasar6.png) :::caution If the app is blank or not refreshing, delete the app and close the simulator, then restart the dev process. ::: 7) Wire up the `updateFile` function: ```ts title="src/pages/IndexPage.vue" import { defineComponent, ref } from 'vue'; // highlight-start import { read, write, utils } from 'xlsx'; import { useQuasar } from 'quasar'; // highlight-end export default defineComponent({ // ... // highlight-start const $q = useQuasar(); function dialogerr(e) { $q.dialog({title: "Error!", message: e.message || String(e)}); } // highlight-end function saveFile() { } async function updateFile(v) { // highlight-start try { const files = (v.target as HTMLInputElement).files; if(!files || files.length == 0) return; const wb = read(await files[0].arrayBuffer()); const data = utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]); todos.value = data.map(row => ({id: row.Index, content: row.Name})); } catch(e) { dialogerr(e); } // highlight-end } ``` To test that reading works: - Download - In the simulator, click the Home icon to return to the home screen - Click on the "Files" icon - Click and drag `pres.numbers` from a Finder window into the simulator. ![Quasar Step 7 save file](pathname:///mobile/quasar7a.png) - Make sure "On My iPhone" is highlighted and select "Save" - Click the Home icon again then select the SheetJSQuasar app - Click the "Load" button, then select "Choose File" and select `pres`: ![Quasar Step 7 load file](pathname:///mobile/quasar7b.png) Once selected, the screen should refresh with new contents: ![Quasar Step 7 new data](pathname:///mobile/quasar7c.png) 8) Wire up the `saveFile` function: ```js function saveFile() { // highlight-start /* generate workbook from state */ const ws = utils.json_to_sheet(todos.value); const wb = utils.book_new(); utils.book_append_sheet(wb, ws, "SheetJSQuasar"); const u8: Uint8Array = write(wb, {bookType: "xlsx", type: "buffer"}); /* save to file */ window.requestFileSystem(window.PERSISTENT, 0, function(fs) { try { fs.root.getFile("SheetJSQuasar.xlsx", {create: true}, entry => { const msg = `File stored at ${$q.cordova.file.documentsDirectory} ${entry.fullPath}`; entry.createWriter(writer => { try { const data = new Blob([u8], {type: "application/vnd.ms-excel"}); writer.onwriteend = () => { try { $q.dialog({title: "Success!", message: msg}); } catch(e) { dialogerr(e); } }; writer.onerror = dialogerr; writer.write(data); } catch(e) { dialogerr(e); } }, dialogerr); }, dialogerr); } catch(e) { dialogerr(e) } }, dialogerr); // highlight-end } ``` The page should revert to the old contents. To test that writing works: - Click "Save File". You will see a popup with a location: ![Quasar Step 8](pathname:///mobile/quasar8.png) - Find the file and verify the contents are correct. Run in a new terminal: ```bash find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx | while read x; do echo "$x"; npx xlsx-cli "$x"; done ``` Since the contents reverted, you should see ``` SheetJSQuasar id,content 1,ct1 2,ct2 3,ct3 4,ct4 5,ct5 ``` - Use "Load File" to select `pres.numbers` again. Wait for the app to refresh. - Click "Save File", then re-run the command: ```bash find ~/Library/Developer/CoreSimulator -name SheetJSQuasar.xlsx | while read x; do echo "$x"; npx xlsx-cli "$x"; done ``` The contents from `pres.numbers` should show up now, with a new header row: ``` SheetJSQuasar id,content 42,Bill Clinton 43,GeorgeW Bush 44,Barack Obama 45,Donald Trump 46,Joseph Biden ```