diff --git a/docs/react/your-first-app.md b/docs/react/your-first-app.md index b1fe24b6ec..c33ad00dc5 100644 --- a/docs/react/your-first-app.md +++ b/docs/react/your-first-app.md @@ -4,13 +4,15 @@ sidebar_label: Build Your First App --- - React Apps | Build Your First Ionic Framework React Application + Build Your First Ionic Mobile App with React | Ionic Capacitor Camera +# Your First Ionic App: React + The great thing about Ionic is that with one codebase, you can build for any platform using just HTML, CSS, and JavaScript. Follow along as we learn the fundamentals of Ionic app development by creating a realistic app step by step. Here’s the finished app running on all 3 platforms: @@ -30,11 +32,11 @@ We'll create a Photo Gallery app that offers the ability to take photos with you Highlights include: -- One React-based codebase that runs on the web, iOS, and Android using Ionic Framework [UI components](https://ionicframework.com/docs/components). +- One React-based codebase that runs on the web, iOS, and Android using Ionic Framework [UI components](../components.md). - Deployed as a native iOS and Android mobile app using [Capacitor](https://capacitorjs.com), Ionic's official native app runtime. -- Photo Gallery functionality powered by the Capacitor [Camera](https://capacitorjs.com/docs/apis/camera), [Filesystem](https://capacitorjs.com/docs/apis/filesystem), and [Preferences](https://capacitorjs.com/docs/apis/preferences) APIs. +- Photo Gallery functionality powered by the Capacitor [Camera](../native/camera.md), [Filesystem](../native/filesystem.md), and [Preferences](../native/preferences.md) APIs. -Find the complete app code referenced in this guide [on GitHub](https://github.com/ionic-team/photo-gallery-capacitor-react). +Find the [complete app code](https://github.com/ionic-team/tutorial-photo-gallery-react) referenced in this guide on GitHub. ## Download Required Tools @@ -43,9 +45,8 @@ Download and install these right away to ensure an optimal Ionic development exp - **Node.js** for interacting with the Ionic ecosystem. [Download the LTS version here](https://nodejs.org/en/). - **A code editor** for... writing code! We are fans of [Visual Studio Code](https://code.visualstudio.com/). - **Command-line interface/terminal (CLI)**: - - **Windows** users: for the best Ionic experience, we recommend the built-in command line (cmd) or the Powershell - CLI, running in Administrator mode. - - **Mac/Linux** users, virtually any terminal will work. + - **Windows** users: for the best Ionic experience, we recommend the built-in command line (cmd) or the Powershell CLI, running in Administrator mode. + - **Mac/Linux** users: virtually any terminal will work. ## Install Ionic Tooling @@ -67,7 +68,7 @@ Consider setting up npm to operate globally without elevated permissions. See [R ## Create an App -Next, create an Ionic React app that uses the “Tabs” starter template and adds Capacitor for native functionality: +Next, create an Ionic React app that uses the "Tabs" starter template and adds Capacitor for native functionality: ```shell ionic start photo-gallery tabs --type=react --capacitor @@ -89,7 +90,7 @@ npm install @capacitor/camera @capacitor/preferences @capacitor/filesystem ### PWA Elements -Some Capacitor plugins, including the Camera API, provide the web-based functionality and UI via the Ionic [PWA Elements library](https://github.com/ionic-team/pwa-elements). +Some Capacitor plugins, including the [Camera API](../native/camera.md), provide the web-based functionality and UI via the Ionic [PWA Elements library](https://github.com/ionic-team/pwa-elements). It's a separate dependency, so install it next: @@ -97,22 +98,32 @@ It's a separate dependency, so install it next: npm install @ionic/pwa-elements ``` -After installation, open up the project in your code editor of choice. - Next, import `@ionic/pwa-elements` by editing `src/main.tsx`. ```tsx +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +// CHANGE: Add the following import. import { defineCustomElements } from '@ionic/pwa-elements/loader'; -// Call the element loader before the render call +// CHANGE: Call the element loader before the render call. defineCustomElements(window); + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render( + + + +); ``` That’s it! Now for the fun part - let’s see the app in action. ## Run the App -Run this command in your shell: +Run this command next: ```shell ionic serve @@ -122,35 +133,52 @@ And voilà! Your Ionic app is now running in a web browser. Most of your app can ## Photo Gallery!!! -There are three tabs. Click on the Tab2 tab. It’s a blank canvas, aka the perfect spot to transform into a Photo Gallery. The Ionic CLI features Live Reload, so when you make changes and save them, the app is updated immediately! +There are three tabs. Click on the "Tab2" tab. It’s a blank canvas, aka the perfect spot to transform into a Photo Gallery. The Ionic CLI features Live Reload, so when you make changes and save them, the app is updated immediately! ![Animated GIF showing the live reload feature in an Ionic app, with changes in code immediately updating the app in a web browser.](/img/guides/react/first-app/live-reload.gif 'Live Reload Feature in Ionic App') Open `/src/pages/Tab2.tsx`. We see: ```tsx - - - - Tab 2 - - - - - - +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2: React.FC = () => { + return ( + + + + Tab 2 + + + + + + Tab 2 + + + + + + ); +}; + +export default Tab2; ``` -`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title. Let’s rename it: +`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title (there are two of them due to iOS [Collapsible Large Title](../api/title.md#collapsible-large-titles) support). Let’s rename both `IonTitle` elements to: ```tsx Photo Gallery ``` -We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB). First, update the imports at the top of the page to include the Camera icon as well as some of the Ionic components we'll use shortly: +We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](../api/fab.md) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx +// CHANGE: Add the following import. import { camera, trash, close } from 'ionicons/icons'; +// CHANGE: Update the following import. import { IonContent, IonHeader, @@ -166,39 +194,101 @@ import { IonImg, IonActionSheet, } from '@ionic/react'; +// CHANGE: Remove or comment out `ExploreContainer`. +// import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2: React.FC = () => { + return ( + + + + Photo Gallery + + + + + + Photo Gallery + + + {/* CHANGE: Add the floating action button. */} + + + + + + + {/* CHANGE: Remove or comment out `ExploreContainer`. */} + {/* */} + + ); +}; + +export default Tab2; ``` -Then, add the FAB to the bottom of the page. Use the camera image as the icon, and call the `takePhoto()` function when this button is clicked (to be implemented soon): - -```tsx - - - takePhoto()}> - - - - -``` - -We’ll be creating the `takePhoto` method and the logic to use the Camera and other native features in a moment. - -Next, open `src/App.tsx`, remove the `ellipse` icon from the import and import the `images` icon instead: +Next, open `src/views/TabsPage.vue`. Change the label to "Photos" and the `ellipse` icon to `images` for the middle tab button. ```tsx +import { Redirect, Route } from 'react-router-dom'; +import { + IonApp, + IonIcon, + IonLabel, + IonRouterOutlet, + IonTabBar, + IonTabButton, + IonTabs, + setupIonicReact, +} from '@ionic/react'; +import { IonReactRouter } from '@ionic/react-router'; +// CHANGE: Update the following import. import { images, square, triangle } from 'ionicons/icons'; +import Tab1 from './pages/Tab1'; +import Tab2 from './pages/Tab2'; +import Tab3 from './pages/Tab3'; + +/* Ionic styles are not shown in this example to keep it brief but will be included in the Ionic package downloaded for your app. Do not remove them. */ + +const App: React.FC = () => ( + + + + + + + + + + + + + + + + + + + + + + {/* CHANGE: Update icon. */} + + + + + + + +); ``` -Within the tab bar (``), change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button: - -```tsx - - - Photos - -``` - -:::note -In Ionic React, icons are imported individually from `ionicons/icons` and set to the icon prop. -::: - That’s just the start of all the cool things we can do with Ionic. Up next, implement camera taking functionality on the web, then build it for iOS and Android. diff --git a/docs/react/your-first-app/2-taking-photos.md b/docs/react/your-first-app/2-taking-photos.md index 64b402d6ae..305970a351 100644 --- a/docs/react/your-first-app/2-taking-photos.md +++ b/docs/react/your-first-app/2-taking-photos.md @@ -4,41 +4,35 @@ sidebar_label: Taking Photos --- - Take Photos From The Camera on React Apps - Ionic Documentation + Take Photos with Camera API for iOS, Android & Web with React | Ionic Capacitor Camera -Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](https://capacitorjs.com/docs/apis/camera). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). +# Taking Photos with the Camera -To do so, we will create our own custom React hook that will manage the photos for the gallery. +Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](../../native/camera.md). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). -:::note -If you are not familiar with React Hooks, [Introducing React Hooks](https://react.dev/reference/react/hooks) from the official React docs is a good resource to start with. -::: +## Photo Gallery Hook + +We will create a [custom React hook](https://react.dev/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component) to manage the photos for the gallery. Create a new file at `src/hooks/usePhotoGallery.ts` and open it up. -A custom hook is just a function that uses other React hooks. And that's what we will be doing! We will start by importing the various hooks and utilities we will be using from React core, the Ionic React Hooks project, and Capacitor: +Next, define a new method, `usePhotoGallery()`, that will contain the core logic to take a device photo and save it to the filesystem. Let’s start by opening the device camera. -```tsx +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; - import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; -``` -Next, create a function named usePhotoGallery: - -```tsx export function usePhotoGallery() { - const takePhoto = async () => { - const photo = await Camera.getPhoto({ + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100, @@ -46,101 +40,229 @@ export function usePhotoGallery() { }; return { - takePhoto, + addNewToGallery, }; } ``` -Our `usePhotoGallery` hook exposes a method called takePhoto, which in turn calls into Capacitor's getPhoto method. - Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The Capacitor Camera plugin abstracts that away for us, leaving just one method call - `getPhoto()` - that will open up the device's camera and allow us to take photos. -The last step we need to take is to use the new hook from the Tab2 page. Go back to Tab2.tsx and import the hook: +Next, in `Tab2.tsx`, import the `usePhotoGallery()` method and destructure it to call its `addNewToGallery()` method. ```tsx +import { camera, trash, close } from 'ionicons/icons'; +import { + IonContent, + IonHeader, + IonPage, + IonTitle, + IonToolbar, + IonFab, + IonFabButton, + IonIcon, + IonGrid, + IonRow, + IonCol, + IonImg, + IonActionSheet, +} from '@ionic/react'; +// CHANGE: Add `usePhotoGallery` import. import { usePhotoGallery } from '../hooks/usePhotoGallery'; -``` +import './Tab2.css'; -And right before the return statement in the functional component, get access to the `takePhoto` method by using the hook: - -```tsx const Tab2: React.FC = () => { - const { takePhoto } = usePhotoGallery(); + // CHANGE: Destructure `addNewToGallery()` from `usePhotoGallery()`. + const { addNewToGallery } = usePhotoGallery(); + + return ( + + + + Photo Gallery + + + + + + Photo Gallery + + + + + {/* CHANGE: Add a click event listener to the floating action button. */} + addNewToGallery()}> + + + + + + ); +}; - // snip - rest of code +export default Tab2; ``` -Save the file, and if you’re not already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! +If it's not running already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! ![A photo gallery app displaying a webcam selfie.](/img/guides/first-app-cap-ng/camera-web.png 'Webcam Selfie in Photo Gallery') _(Your selfie is probably much better than mine)_ -After taking a photo, it disappears. We still need to display it within our app and save it for future access. +After taking a photo, it disappears right away. We need to display it within our app and save it for future access. ## Displaying Photos -First we will create a new type to define our Photo, which will hold specific metadata. Add the following UserPhoto interface to the `usePhotoGallery.ts` file, somewhere outside of the main function: +Return to `usePhotoGallery.ts`. -```tsx +Outside of the `usePhotoGallery()` method definition (the very bottom of the file), create a new interface, `UserPhoto`, to hold our photo metadata. + +```ts +export function usePhotoGallery { + // Same old code from before. +} + +// CHANGE: Add the `UserPhoto` interface. export interface UserPhoto { filepath: string; webviewPath?: string; } ``` -Back at the top of the function (right after the call to `usePhotoGallery`, we will define a state variable to store the array of each photo captured with the Camera. +Above the `addNewToGallery()` method, define an array of `UserPhoto`, which will contain a reference to each photo captured with the Camera. Make it a state variable using React's [useState hook](https://react.dev/reference/react/useState). -```tsx -const [photos, setPhotos] = useState([]); +```ts +export function usePhotoGallery { + // CHANGE: Add the `photos` array. + const [photos, setPhotos] = useState([]); + + // Same old code from before. +} ``` -When the camera is done taking a picture, the resulting Photo returned from Capacitor will be stored in the `photo` variable. We want to create a new photo object and add it to the photos state array. We make sure we don't accidentally mutate the current photos array by making a new array, and then call `setPhotos` to store the array into state. Update the `takePhoto` method and add this code after the getPhoto call: +Over in the `addNewToGallery()` method, add the newly captured photo to the beginning of the `photos` array. Then, update the `userPhotoGallery()` return statement with the `photos` array. -```tsx -const fileName = Date.now() + '.jpeg'; -const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, -]; -setPhotos(newPhotos); -``` +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); -Next, let's expose the photos array from our hook. Update the return statement to include the photos: + // CHANGE: Create the `fileName` with current timestamp. + const fileName = Date.now() + '.jpeg'; + // CHANGE: Create `savedImageFile` matching `UserPhoto` interface. + const savedImageFile = [ + { + filepath: fileName, + webviewPath: capturedPhoto.webPath, + }, + ...photos, + ]; + + // CHANGE: Update the `photos` array with the new photo. + setPhotos(savedImageFile); + }; -```tsx -return { - photos, - takePhoto, -}; + return { + addNewToGallery, + // CHANGE: Update return statement to include `photos` array. + photos, + }; +} ``` -And back in the Tab2 component, get access to the photos: +`usePhotoGallery.ts` should now look like this: -```tsx -const { photos, takePhoto } = usePhotoGallery(); +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + const savedImageFile = [ + { + filepath: fileName, + webviewPath: capturedPhoto.webPath, + }, + ...photos, + ]; + + setPhotos(savedImageFile); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path: +Next, switch to `Tab2.tsx` to display the images. We'll add a [Grid component](../../api/grid.md) to ensure the photos display neatly as they're added to the gallery. Inside the grid, loop through each photo in the `UserPhoto`'s `photos` array. For each item, add an [Image component](../../api/img.md) and set its `src` property to the photo's path. ```tsx - - - - {photos.map((photo, index) => ( - - - - ))} - - - - +const Tab2: React.FC = () => { + // CHANGE: Add `photos` array to destructure from `usePhotoGallery()`. + const { photos, addNewToGallery } = usePhotoGallery(); + + return ( + + + + Photo Gallery + + + + + + Photo Gallery + + + + {/* CHANGE: Add a grid component to display the photos. */} + + + {/* CHANGE: Create a new column and image component for each photo. */} + {photos.map((photo) => ( + + + + ))} + + + + + addNewToGallery()}> + + + + + + ); +}; ``` -Save all files. Within the web browser, click the Camera button and take another photo. This time, the photo is displayed in the Photo Gallery! +Within the web browser, click the camera button and take another photo. This time, the photo is displayed in the Photo Gallery! Up next, we’ll add support for saving the photos to the filesystem, so they can be retrieved and displayed in our app at a later time. diff --git a/docs/react/your-first-app/3-saving-photos.md b/docs/react/your-first-app/3-saving-photos.md index eae9a61df7..a1ec2b70c8 100644 --- a/docs/react/your-first-app/3-saving-photos.md +++ b/docs/react/your-first-app/3-saving-photos.md @@ -1,25 +1,124 @@ --- +title: Saving Photos to the Filesystem sidebar_label: Saving Photos --- + + Saving Photos to the Filesystem with React | Ionic Capacitor Camera + + + # Saving Photos to the Filesystem -We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be lost. +We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be deleted. ## Filesystem API -Fortunately, saving them to the filesystem only takes a few steps. Begin by opening the `usePhotoGallery` hook (`src/hooks/usePhotoGallery.ts`), and get access to the `writeFile` method from the `Filesystem` class: +Fortunately, saving them to the filesystem only takes a few steps. Begin by creating a new class method, `savePicture()`, in the `usePhotoGallery()` method. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -:::note -We will use the `writeFile` method initially, but we will use the others coming up shortly, so we'll go ahead and import them now. -::: +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Add the `savePicture()` method. + const savePicture = async (photo: Photo, fileName: string): Promise => { + return { + filepath: 'soon...', + webviewPath: 'soon...', + }; + }; -Next, create a couple of new functions in `usePhotoGallery`: + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +We can use this new method immediately in `addNewToGallery()`. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -```tsx export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + // CHANGE: Add `savedImageFile`. + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + // CHANGE: Update state with new photo. + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + }; + + // CHANGE: Add `savePicture()` method. + const savePicture = async (photo: Photo, fileName: string): Promise => { + return { + filepath: 'soon...', + webviewPath: 'soon...', + }; + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +We'll use the Capacitor [Filesystem API](../../native/filesystem.md) to save the photo. First, convert the photo to base64 format. + +Then, pass the data to the Filesystem's `writeFile` method. Recall that we display photos by setting the image's source path (`src`) to the `webviewPath` property. So, set the `webviewPath` and return the new `Photo` object. + +For now, create a new helper method, `convertBlobToBase64()`, to implement the necessary logic for running on the web. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Update the `savePicture()` method. const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, @@ -33,49 +132,101 @@ export function usePhotoGallery() { webviewPath: photo.webPath, }; }; -} -export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { + // CHANGE: Add `convertBlobToBase64()` method. + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; } ``` -:::note -The base64FromPath method is a helper util that downloads a file from the supplied path and returns a base64 representation of that file. -::: +`usePhotoGallery.ts` should now look like this: -We pass in the `photo` object, which represents the newly captured device photo, as well as the fileName, which will provide a path for the file to be stored to. +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -Next we use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/filesystem) to save the photo to the filesystem. We start by converting the photo to base64 format, then feed the data to the Filesystem’s `writeFile` function. +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); -Last, call `savePicture` and pass in the photo object and filename directly underneath the call to `setPhotos` in the `takePhoto` method. Here is the full method: + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); -```tsx -const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + }; - const fileName = Date.now() + '.jpeg'; - const savedFileImage = await savePicture(photo, fileName); - const newPhotos = [savedFileImage, ...photos]; - setPhotos(newPhotos); -}; + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. +Obtaining the camera photo as base64 format on the web appears to be a bit trickier than on mobile. In reality, we’re just using built-in web APIs: [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) as a neat way to read the file into blob format, then FileReader’s [readAsDataURL()](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) to convert the photo blob to base64. + +There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. Next up, we'll load and display our saved images. diff --git a/docs/react/your-first-app/4-loading-photos.md b/docs/react/your-first-app/4-loading-photos.md index 75c1cb8e90..050fbc9e51 100644 --- a/docs/react/your-first-app/4-loading-photos.md +++ b/docs/react/your-first-app/4-loading-photos.md @@ -4,62 +4,199 @@ sidebar_label: Loading Photos --- - Loading Photos from the Filesystem Using A Key-Value Store + Loading Photos from the Filesystem with React | Ionic Capacitor Camera +# Loading Photos from the Filesystem + We’ve implemented photo taking and saving to the filesystem. There’s one last piece of functionality missing: the photos are stored in the filesystem, but we need a way to save pointers to each file so that they can be displayed again in the photo gallery. -Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](https://capacitorjs.com/docs/apis/preferences) to store our array of Photos in a key-value store. +Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](../../native/preferences.md) to store our array of Photos in a key-value store. ## Preferences API -Begin by defining a constant variable that will act as the key for the store before the `usePhotoGallery` function definition in `src/hooks/usePhotoGallery.ts`: +Open `usePhotoGallery.ts` and begin by defining a constant variable that will act as the key for the store. + +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + // CHANGE: Add a key for photo storage. + const PHOTO_STORAGE = 'photos'; -```tsx -const PHOTO_STORAGE = 'photos'; -export function usePhotoGallery() {} + // Same old code from before. +} ``` -Then, use the `Storage` class to get access to the get and set methods for reading and writing to device storage: +Next, at the end of the `addNewToGallery()` method, add a call to the `Preferences.set()` method to save the `photos` array. By adding it here, the `photos` array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. -At the end of the `takePhoto` function, add a call to `Preferences.set()` to save the Photos array. By adding it here, the Photos array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. +```ts +const addNewToGallery = async () => { + // Same old code from before. -```tsx -Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + // CHANGE: Add method to cache all photo data for future retrieval. + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); +}; ``` -With the photo array data saved, we will create a method that will retrieve the data when the hook loads. We will do so by using React's `useEffect` hook. Insert this above the `takePhoto` declaration. Here is the code, and we will break it down: - -```tsx -useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - setPhotos(photosInPreferences); - }; - loadSaved(); -}, []); +With the photo array data saved, create a new method in the `usePhotoGallery()` called `loadSaved()` that can retrieve the photo data. We use the same key to retrieve the `photos` array in JSON format, then parse it into an array. + +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + // CHANGE: Add useEffect hook. + useEffect(() => { + // CHANGE: Add `loadSaved()` method. + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + }; + + loadSaved(); + }, []); + + // Same old code from before. +} ``` -This seems a bit scary at first, so let's walk through it, first by looking at the second parameter we pass into the hook: the dependency array `[]`. +The second parameter, the empty dependency array (`[]`), is what tells React to only run the function once. Normally, [useEffect hooks](https://react.dev/reference/react/useEffect) run after every render, but passing an empty array prevents it from running again because none of the dependencies, the values the hook relies on, will ever change. + +On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the `Filesystem`, displaying them automatically. On the web, however, we must read each image from the `Filesystem` into base64 format, using a new `base64` property on the `Photo` object. This is because the `Filesystem` API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Add the following code to complete the `loadSaved()` method. + +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + useEffect(() => { + // CHANGE: Update `loadSaved()` method. + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // CHANGE: Display the photo by reading into base64 format. + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + setPhotos(photosInPreferences); + }; + + loadSaved(); + }, []); + + // Same old code from before. +} +``` + +`usePhotoGallery.ts` should now look like this: + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -The useEffect hook, by default, gets called each time a component renders, unless, we pass in a dependency array. In that case, it will only run when a dependency gets updated. In our case we only want it to be called once. By passing in an empty array, which will not be changed, we can prevent the hook from being called multiple times. +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + useEffect(() => { + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + setPhotos(photosInPreferences); + }; + + loadSaved(); + }, []); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` -The first parameter to `useEffect` is the function that will be called by the effect. We pass in an anonymous arrow function, and inside of it we define another asynchronous method and then immediately call this. We have to call the async function from within the hook as the hook callback can't be asynchronous itself. +:::note +If you're seeing broken image links or missing photos after following these steps, you may need to open your browser's dev tools and clear both [localStorage](https://developer.chrome.com/docs/devtools/storage/localstorage) and [IndexedDB](https://developer.chrome.com/docs/devtools/storage/indexeddb). -On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the Filesystem, displaying them automatically. On the web, however, we must read each image from the Filesystem into base64 format, because the Filesystem API stores them in base64 within [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. +In localStorage, look for domain `http://localhost:8100` and key `CapacitorStorage.photos`. In IndexedDB, find a store called "FileStorage". Your photos will have a key like `/DATA/123456789012.jpeg`. +::: That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android! diff --git a/docs/react/your-first-app/5-adding-mobile.md b/docs/react/your-first-app/5-adding-mobile.md index fa1799da9d..1b2915d2d1 100644 --- a/docs/react/your-first-app/5-adding-mobile.md +++ b/docs/react/your-first-app/5-adding-mobile.md @@ -1,38 +1,73 @@ --- +title: Adding Mobile sidebar_label: Adding Mobile --- + + Adding Mobile Support with React | Ionic Capacitor Camera + + + # Adding Mobile Our photo gallery app won’t be complete until it runs on iOS, Android, and the web - all using one codebase. All it takes is some small logic changes to support mobile platforms, installing some native tooling, then running the app on a device. Let’s go! +## Import Platform API + Let’s start with making some small code changes - then our app will “just work” when we deploy it to a device. +Import the Ionic [Platform API](../platform.md) into `usePhotoGallery.ts`, which is used to retrieve information about the current device. In this case, it’s useful for selecting which code to execute based on the platform the app is running on (web or mobile). + +Add `isPlatform` to the imports at the top of the file to use the `isPlatform` method. `Capacitor` is also imported to help with file paths on mobile devices. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +// CHANGE: Add imports. +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; + +// Same old code from before. +``` + ## Platform-specific Logic -First, we’ll update the photo saving functionality to support mobile. In the `savePicture` function, check which platform the app is running on. If it’s “hybrid” (Capacitor or Cordova, the two native runtimes), then read the photo file into base64 format using the `readFile` method. Also, return the complete file path to the photo using the Filesystem API. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc` method ([details here](https://ionicframework.com/docs/core-concepts/webview#file-protocol)). Otherwise, use the same logic as before when running the app on the web. +First, we’ll update the photo saving functionality to support mobile. In the `savePicture()` method, check which platform the app is running on. If it’s “hybrid” (Capacitor, the native runtime), then read the photo file into base64 format using the `Filesystem`'s' `readFile()` method. Otherwise, use the same logic as before when running the app on the web. + +Update `savePicture()` to look like the following: -```tsx +```ts +// CHANGE: Update the `savePicture()` method. const savePicture = async (photo: Photo, fileName: string): Promise => { let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor; + // CHANGE: Add platform check. + // "hybrid" will detect mobile - iOS or Android if (isPlatform('hybrid')) { const file = await Filesystem.readFile({ path: photo.path!, }); base64Data = file.data; } else { - base64Data = await base64FromPath(photo.webPath!); + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; } + const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, directory: Directory.Data, }); + // CHANGE: Add platform check. if (isPlatform('hybrid')) { // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol return { filepath: savedFile.uri, webviewPath: Capacitor.convertFileSrc(savedFile.uri), @@ -48,26 +83,145 @@ const savePicture = async (photo: Photo, fileName: string): Promise = }; ``` -Next, add a new bit of logic in the `loadSaved` function. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved` function inside of `useEffect` to: +Next, add a new bit of logic in the `loadSaved()` method. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved()` method: -```tsx +```ts +// CHANGE: Update `loadSaved` method. const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + // CHANGE: Add platform check. // If running on the web... if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { + for (const photo of photosInPreferences) { const file = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); - // Web platform only: Load the photo as base64 data photo.webviewPath = `data:image/jpeg;base64,${file.data}`; } } + setPhotos(photosInPreferences); }; ``` -Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. Next up, the part you’ve been waiting for - deploying the app to a device. +Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. + +`usePhotoGallery.ts` should now look like this: + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + useEffect(() => { + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // If running on the web... + if (!isPlatform('hybrid')) { + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + + setPhotos(photosInPreferences); + }; + + loadSaved(); + }, []); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect mobile - iOS or Android + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; + } + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + }; + + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Next up, the part you’ve been waiting for - deploying the app to a device. diff --git a/docs/react/your-first-app/6-deploying-mobile.md b/docs/react/your-first-app/6-deploying-mobile.md index da780fa76a..d6101c33d0 100644 --- a/docs/react/your-first-app/6-deploying-mobile.md +++ b/docs/react/your-first-app/6-deploying-mobile.md @@ -1,10 +1,23 @@ --- +title: Deploying to iOS and Android sidebar_label: Deploying Mobile --- + + Adding Mobile Support with React | Ionic Capacitor Camera + + + # Deploying to iOS and Android -Since we added Capacitor to our project when it was first created, there’s only a handful of steps remaining until the Photo Gallery app is on our device! Remember, you can find the complete source code for this app [here](https://github.com/ionic-team/photo-gallery-capacitor-react). +Since we added Capacitor to our project when it was first created, there’s only a handful of steps remaining until the Photo Gallery app is on our device! + +:::note +Remember, you can find the complete source code for this app [here](https://github.com/ionic-team/photo-gallery-capacitor-react). +::: ## Capacitor Setup @@ -19,8 +32,8 @@ ionic build Next, create both the iOS and Android projects: ```shell -$ ionic cap add ios -$ ionic cap add android +ionic cap add ios +ionic cap add android ``` Both android and ios folders at the root of the project are created. These are entirely standalone native projects that should be considered part of your Ionic app (i.e., check them into source control, edit them using their native tooling, etc.). @@ -43,7 +56,7 @@ ionic cap sync To build an iOS app, you’ll need a Mac computer. ::: -Capacitor iOS apps are configured and managed through Xcode (Apple’s iOS/Mac IDE), with dependencies managed by CocoaPods. Before running this app on an iOS device, there's a couple of steps to complete. +Capacitor iOS apps are configured and managed through Xcode (Apple’s iOS/Mac IDE), with dependencies managed by [CocoaPods](https://cocoapods.org/). Before running this app on an iOS device, there's a couple of steps to complete. First, run the Capacitor `open` command, which opens the native iOS project in Xcode: @@ -51,7 +64,7 @@ First, run the Capacitor `open` command, which opens the native iOS project in X ionic cap open ios ``` -In order for some native plugins to work, user permissions must be configured. In our photo gallery app, this includes the Camera plugin: iOS displays a modal dialog automatically after the first time that `Camera.getPhoto()` is called, prompting the user to allow the app to use the Camera. The permission that drives this is labeled “Privacy - Camera Usage.” To set it, the `Info.plist` file must be modified ([more details here](https://capacitorjs.com/docs/ios/configuration)). To access it, click "Info," then expand "Custom iOS Target Properties." +In order for some native plugins to work, user permissions must be configured. In our photo gallery app, this includes the Camera plugin: iOS displays a modal dialog automatically after the first time that `Camera.getPhoto()` is called, prompting the user to allow the app to use the Camera. The permission that drives this is labeled "Privacy - Camera Usage." To set it, the `Info.plist` file must be modified ([more details here](https://capacitorjs.com/docs/ios/configuration)). To access it, click "Info," then expand "Custom iOS Target Properties." ![The Info.plist file in Xcode showing the NSCameraUsageDescription key added for camera access.](/img/guides/first-app-cap-ng/xcode-info-plist.png 'Xcode Info.plist Configuration') diff --git a/docs/react/your-first-app/7-live-reload.md b/docs/react/your-first-app/7-live-reload.md index 15d4f643f2..3652b25f53 100644 --- a/docs/react/your-first-app/7-live-reload.md +++ b/docs/react/your-first-app/7-live-reload.md @@ -1,120 +1,162 @@ --- +title: Rapid App Development with Live Reload sidebar_label: Live Reload --- + + Rapid App Development with Live Reload with React | Ionic Capacitor Camera + + + # Rapid App Development with Live Reload So far, we’ve seen how easy it is to develop a cross-platform app that works everywhere. The development experience is pretty quick, but what if I told you there was a way to go faster? -We can use the Ionic CLI’s [Live Reload functionality](https://ionicframework.com/docs/cli/livereload) to boost our productivity when building Ionic apps. When active, Live Reload will reload the browser and/or WebView when changes in the app are detected. +We can use the Ionic CLI’s [Live Reload functionality](../../cli/livereload.md) to boost our productivity when building Ionic apps. When active, Live Reload will reload the browser and/or WebView when changes in the app are detected. ## Live Reload Remember `ionic serve`? That was Live Reload working in the browser, allowing us to iterate quickly. -We can also use it when developing on iOS and Android devices. This is particularly useful when writing code that interacts with native plugins. Since we need to run native plugin code on a device in order to verify that it works, having a way to quickly write code, build and deploy it, then test it is crucial to keeping up our development speed. +We can also use it when developing on iOS and Android devices. This is particularly useful when writing code that interacts with native plugins - we must run it on a device to verify that it works. Therefore, being able to quickly write, build, test, and deploy code is crucial to keeping up our development speed. Let’s use Live Reload to implement photo deletion, the missing piece of our Photo Gallery feature. Select your platform of choice (iOS or Android) and connect a device to your computer. Next, run either command in a terminal, based on your chosen platform: ```shell -$ ionic cap run ios -l --external +ionic cap run ios -l --external -$ ionic cap run android -l --external +ionic cap run android -l --external ``` The Live Reload server will start up, and the native IDE of choice will open if not opened already. Within the IDE, click the Play button to launch the app onto your device. ## Deleting Photos -With Live Reload running and the app open on your device, let’s implement photo deletion functionality. In your code editor (not Android Studio or Xcode), open `Tab2.tsx` then import `useState` from React and `UserPhoto` from the `usePhotoGallery` hook: - -```tsx -import React, { useState } from 'react'; -import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; -// other imports -``` - -Next, reference the `deletePhoto` function, which we'll create soon: - -```tsx -const { photos, takePhoto, deletePhoto } = usePhotoGallery(); -``` - -Next, add a state value to store information about the photo to delete: - -```tsx -const [photoToDelete, setPhotoToDelete] = useState(); -``` - -When a user clicks on an image, we will show the action sheet by changing the state value to the photo. Update the `` element to: - -```tsx - setPhotoToDelete(photo)} src={photo.webviewPath} /> -``` - -Next, add an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) dialog with the option to either delete the selected photo or cancel (close) the dialog. We will set the isOpen property based on if photoToDelete has a value or not. - -In the JSX, put the following component before the closing `` tag. - -```tsx - { - if (photoToDelete) { - deletePhoto(photoToDelete); - setPhotoToDelete(undefined); - } - }, - }, - { - text: 'Cancel', - icon: close, - role: 'cancel', - }, - ]} - onDidDismiss={() => setPhotoToDelete(undefined)} -/> +With Live Reload running and the app open on your device, let’s implement photo deletion functionality. + +In `usePhotoGallery.ts`, add the `deletePhoto()` method. The selected photo is removed from the `photos` array first. Then, we delete the actual photo file itself using the Filesystem API. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; + +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Add `deletePhoto()` method. + const deletePhoto = async (photo: UserPhoto) => { + // Remove this photo from the Photos reference data array + const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); + + // Update photos array cache by overwriting the existing photo array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + + // Delete photo file from filesystem + const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); + await Filesystem.deleteFile({ + path: filename, + directory: Directory.Data, + }); + + setPhotos(newPhotos); + }; + + return { + photos, + addNewToGallery, + // CHANGE: Add `deletePhoto()` to the return statement. + deletePhoto, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -Above, we added two options: `Delete` that calls `deletePhoto` function (to be added next) and `Cancel`, which when given the role of “cancel” will automatically close the action sheet. It's also important to set the onDidDismiss function and set our photoToDelete back to undefined when the modal goes away. That way, when another image is clicked, the action sheet notices the change in the value of photoToDelete. - -Next, we need to implement the deletePhoto method that will come from the `usePhotoGallery` hook. Open the file and paste in the following function in the hook: +Next, in `Tab2.tsx`, implement the `IonActionSheet` component. We're adding two options: "Delete", which calls `usePhotoGallery.deletePicture()`, and "Cancel". The cancel button will automatically closes the action sheet when assigned the "cancel" role. ```tsx -const deletePhoto = async (photo: UserPhoto) => { - // Remove this photo from the Photos reference data array - const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); - - // Update photos array cache by overwriting the existing photo array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - - // delete photo file from filesystem - const filename = photo.filepath.substring(photo.filepath.lastIndexOf('/') + 1); - await Filesystem.deleteFile({ - path: filename, - directory: Directory.Data, - }); - setPhotos(newPhotos); +// Same old code from before. +// change: Add React import. +import { useState } from 'react'; +// CHANGE: Add `UserPhoto` type import. +import type { UserPhoto } from '../hooks/usePhotoGallery'; + +const Tab2: React.FC = () => { + // CHANGE: Add `deletePhoto()` method. + const { photos, addNewToGallery, deletePhoto } = usePhotoGallery(); + // CHANGE: Add state for the photo to delete. + const [photoToDelete, setPhotoToDelete] = useState(); + + return ( + + + {/* Same old code from before. */} + + + addNewToGallery()}> + + + + + {/* CHANGE: Add action sheet for deleting photos. */} + { + if (photoToDelete) { + deletePhoto(photoToDelete); + setPhotoToDelete(undefined); + } + }, + }, + { + text: 'Cancel', + icon: close, + role: 'cancel', + handler: () => { + // Nothing to do, action sheet is automatically closed + }, + }, + ]} + onDidDismiss={() => setPhotoToDelete(undefined)} + > + + + ); }; ``` -The selected photo is removed from the Photos array first. Then, we use the Capacitor Preferences API to update the cached version of the Photos array. Finally, we delete the actual photo file itself using the Filesystem API. - -Make sure to return the `deletePhoto` function so it is as a part of the hook API that we expose: +Add a click handler to the `` element. When the app user taps on a photo in our gallery, we’ll display an [Action Sheet](../../api/action-sheet.md) dialog with the option to either delete the selected photo or cancel (close) the dialog. ```tsx -return { - photos, - takePhoto, - deletePhoto, -}; + + + {photos.map((photo) => ( + + {/* CHANGE: Add a click event listener to each image. */} + setPhotoToDelete(photo)} /> + + ))} + + ``` -Save this file, then tap on a photo again and choose the “Delete” option. This time, the photo is deleted! Implemented much faster using Live Reload. 💪 +Remember that removing the photo from the `photos` array triggers the `cachePhotos` method for us automatically. + +Tap on a photo again and choose the “Delete” option. The photo is deleted! Implemented much faster using Live Reload. 💪 In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. diff --git a/docs/react/your-first-app/8-distribute.md b/docs/react/your-first-app/8-distribute.md index 398f572b64..1ebffeceda 100644 --- a/docs/react/your-first-app/8-distribute.md +++ b/docs/react/your-first-app/8-distribute.md @@ -1,7 +1,16 @@ --- +title: Build and Deploy your App sidebar_label: Distribute --- + + Build and Deploy your App with React | Ionic Capacitor Camera + + + # Build and Deploy your App Now that you have built your first app, you are going to want to get it distributed so everyone can start using it. The mechanics of building and deploying your application can be quite cumbersome. That is where [Appflow](https://ionic.io/docs/appflow/) comes into play. Appflow allows you to effectively generate web and native builds, push out live app updates, publish your app to the app stores, and automate the whole process. The entire Quickstart guide can be found [here](https://ionic.io/docs/appflow/quickstart). @@ -63,7 +72,7 @@ To dive into more details on the steps to deploy a live update, as well as addit Next up is a native binary for your app build and deploy process. This is done via the [Ionic Package](https://ionic.io/docs/appflow/package/intro) service. First things first, you will need to create a [Package build](https://ionic.io/docs/appflow/package/builds). This can be done by clicking the `Start build` icon from the `Commits` tab or by clicking the `New build` button in the top right from the `Build > Builds` tab. Then you will select the proper commit for your build and fill in all of the several required fields and any optional fields that you want to specify. After filling in all of the information and the build begins, you can check out it's progress and review the logs if you encounter any errors. -Given a successful Package build, and iOS binary (`.ipa` or IPA) or and Android binary (`.apk` or APK) file becomes available to you. The file can subsequently be downloaded so you can install it on a device by clicking the file name in the `Artifacts` section in the right of the build detail page or clicking the `Download IPA/APK` icon on the build in the `Build > Builds` tab. +Given a successful Package build, an iOS binary (`.ipa` or IPA) or/and an Android binary (`.apk` or APK) file becomes available to you. The file can subsequently be downloaded so you can install it on a device by clicking the file name in the `Artifacts` section in the right of the build detail page or clicking the `Download IPA/APK` icon on the build in the `Build > Builds` tab. Further information regarding building native binaries can be found inside of the [Build a Native Binary](https://ionic.io/docs/appflow/quickstart/package) section inside the Appflow docs. @@ -93,8 +102,8 @@ For access to the ability to create a Native Configuration, you will need to be ## What’s Next? -Congratulations! You developed a complete cross-platform Photo Gallery app that runs on the web, iOS, and Android. Not only that, you have also then built the app and deployed it to you users devices! +Congratulations! You developed a complete cross-platform Photo Gallery app that runs on the web, iOS, and Android. Not only that, you have also then built the app and deployed it to your users' devices! -There are many paths to follow from here. Try adding another [Ionic UI component](https://ionicframework.com/docs/components) to the app, or more [native functionality](https://capacitorjs.com/docs/apis). The sky’s the limit. Once you have added another feature run the build and deploy process again through Appflow to get it out to your users. +There are many paths to follow from here. Try adding another [Ionic UI component](../../components.md) to the app, or more [native functionality](https://capacitorjs.com/docs/apis). The sky’s the limit. Once you have added another feature, run the build and deploy process again through Appflow to get it out to your users. Happy app building! 💙 diff --git a/versioned_docs/version-v7/react/your-first-app.md b/versioned_docs/version-v7/react/your-first-app.md index ca067a3cd3..c33ad00dc5 100644 --- a/versioned_docs/version-v7/react/your-first-app.md +++ b/versioned_docs/version-v7/react/your-first-app.md @@ -4,13 +4,15 @@ sidebar_label: Build Your First App --- - React Apps | Build Your First Ionic Framework React Application + Build Your First Ionic Mobile App with React | Ionic Capacitor Camera +# Your First Ionic App: React + The great thing about Ionic is that with one codebase, you can build for any platform using just HTML, CSS, and JavaScript. Follow along as we learn the fundamentals of Ionic app development by creating a realistic app step by step. Here’s the finished app running on all 3 platforms: @@ -19,9 +21,9 @@ Here’s the finished app running on all 3 platforms: width="560" height="315" src="https://www.youtube.com/embed/0ASQ13Y1Rk4" - frameborder="0" + frameBorder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" - allowfullscreen + allowFullScreen > ## What We'll Build @@ -30,11 +32,11 @@ We'll create a Photo Gallery app that offers the ability to take photos with you Highlights include: -- One React-based codebase that runs on the web, iOS, and Android using Ionic Framework [UI components](https://ionicframework.com/docs/components). +- One React-based codebase that runs on the web, iOS, and Android using Ionic Framework [UI components](../components.md). - Deployed as a native iOS and Android mobile app using [Capacitor](https://capacitorjs.com), Ionic's official native app runtime. -- Photo Gallery functionality powered by the Capacitor [Camera](https://capacitorjs.com/docs/apis/camera), [Filesystem](https://capacitorjs.com/docs/apis/filesystem), and [Preferences](https://capacitorjs.com/docs/apis/preferences) APIs. +- Photo Gallery functionality powered by the Capacitor [Camera](../native/camera.md), [Filesystem](../native/filesystem.md), and [Preferences](../native/preferences.md) APIs. -Find the complete app code referenced in this guide [on GitHub](https://github.com/ionic-team/photo-gallery-capacitor-react). +Find the [complete app code](https://github.com/ionic-team/tutorial-photo-gallery-react) referenced in this guide on GitHub. ## Download Required Tools @@ -43,9 +45,8 @@ Download and install these right away to ensure an optimal Ionic development exp - **Node.js** for interacting with the Ionic ecosystem. [Download the LTS version here](https://nodejs.org/en/). - **A code editor** for... writing code! We are fans of [Visual Studio Code](https://code.visualstudio.com/). - **Command-line interface/terminal (CLI)**: - - **Windows** users: for the best Ionic experience, we recommend the built-in command line (cmd) or the Powershell - CLI, running in Administrator mode. - - **Mac/Linux** users, virtually any terminal will work. + - **Windows** users: for the best Ionic experience, we recommend the built-in command line (cmd) or the Powershell CLI, running in Administrator mode. + - **Mac/Linux** users: virtually any terminal will work. ## Install Ionic Tooling @@ -67,7 +68,7 @@ Consider setting up npm to operate globally without elevated permissions. See [R ## Create an App -Next, create an Ionic React app that uses the “Tabs” starter template and adds Capacitor for native functionality: +Next, create an Ionic React app that uses the "Tabs" starter template and adds Capacitor for native functionality: ```shell ionic start photo-gallery tabs --type=react --capacitor @@ -89,7 +90,7 @@ npm install @capacitor/camera @capacitor/preferences @capacitor/filesystem ### PWA Elements -Some Capacitor plugins, including the Camera API, provide the web-based functionality and UI via the Ionic [PWA Elements library](https://github.com/ionic-team/pwa-elements). +Some Capacitor plugins, including the [Camera API](../native/camera.md), provide the web-based functionality and UI via the Ionic [PWA Elements library](https://github.com/ionic-team/pwa-elements). It's a separate dependency, so install it next: @@ -97,22 +98,32 @@ It's a separate dependency, so install it next: npm install @ionic/pwa-elements ``` -After installation, open up the project in your code editor of choice. - Next, import `@ionic/pwa-elements` by editing `src/main.tsx`. ```tsx +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App'; +// CHANGE: Add the following import. import { defineCustomElements } from '@ionic/pwa-elements/loader'; -// Call the element loader before the render call +// CHANGE: Call the element loader before the render call. defineCustomElements(window); + +const container = document.getElementById('root'); +const root = createRoot(container!); +root.render( + + + +); ``` That’s it! Now for the fun part - let’s see the app in action. ## Run the App -Run this command in your shell: +Run this command next: ```shell ionic serve @@ -122,35 +133,52 @@ And voilà! Your Ionic app is now running in a web browser. Most of your app can ## Photo Gallery!!! -There are three tabs. Click on the Tab2 tab. It’s a blank canvas, aka the perfect spot to transform into a Photo Gallery. The Ionic CLI features Live Reload, so when you make changes and save them, the app is updated immediately! +There are three tabs. Click on the "Tab2" tab. It’s a blank canvas, aka the perfect spot to transform into a Photo Gallery. The Ionic CLI features Live Reload, so when you make changes and save them, the app is updated immediately! ![Animated GIF showing the live reload feature in an Ionic app, with changes in code immediately updating the app in a web browser.](/img/guides/react/first-app/live-reload.gif 'Live Reload Feature in Ionic App') Open `/src/pages/Tab2.tsx`. We see: ```tsx - - - - Tab 2 - - - - - - +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2: React.FC = () => { + return ( + + + + Tab 2 + + + + + + Tab 2 + + + + + + ); +}; + +export default Tab2; ``` -`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title. Let’s rename it: +`IonHeader` represents the top navigation and toolbar, with "Tab 2" as the title (there are two of them due to iOS [Collapsible Large Title](../api/title.md#collapsible-large-titles) support). Let’s rename both `IonTitle` elements to: ```tsx Photo Gallery ``` -We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](https://ionicframework.com/docs/api/fab) (FAB). First, update the imports at the top of the page to include the Camera icon as well as some of the Ionic components we'll use shortly: +We put the visual aspects of our app into ``. In this case, it’s where we’ll add a button that opens the device’s camera as well as displays the image captured by the camera. Start by adding a [floating action button](../api/fab.md) (FAB) to the bottom of the page and set the camera image as the icon. ```tsx +// CHANGE: Add the following import. import { camera, trash, close } from 'ionicons/icons'; +// CHANGE: Update the following import. import { IonContent, IonHeader, @@ -166,39 +194,101 @@ import { IonImg, IonActionSheet, } from '@ionic/react'; +// CHANGE: Remove or comment out `ExploreContainer`. +// import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2: React.FC = () => { + return ( + + + + Photo Gallery + + + + + + Photo Gallery + + + {/* CHANGE: Add the floating action button. */} + + + + + + + {/* CHANGE: Remove or comment out `ExploreContainer`. */} + {/* */} + + ); +}; + +export default Tab2; ``` -Then, add the FAB to the bottom of the page. Use the camera image as the icon, and call the `takePhoto()` function when this button is clicked (to be implemented soon): - -```tsx - - - takePhoto()}> - - - - -``` - -We’ll be creating the `takePhoto` method and the logic to use the Camera and other native features in a moment. - -Next, open `src/App.tsx`, remove the `ellipse` icon from the import and import the `images` icon instead: +Next, open `src/views/TabsPage.vue`. Change the label to "Photos" and the `ellipse` icon to `images` for the middle tab button. ```tsx +import { Redirect, Route } from 'react-router-dom'; +import { + IonApp, + IonIcon, + IonLabel, + IonRouterOutlet, + IonTabBar, + IonTabButton, + IonTabs, + setupIonicReact, +} from '@ionic/react'; +import { IonReactRouter } from '@ionic/react-router'; +// CHANGE: Update the following import. import { images, square, triangle } from 'ionicons/icons'; +import Tab1 from './pages/Tab1'; +import Tab2 from './pages/Tab2'; +import Tab3 from './pages/Tab3'; + +/* Ionic styles are not shown in this example to keep it brief but will be included in the Ionic package downloaded for your app. Do not remove them. */ + +const App: React.FC = () => ( + + + + + + + + + + + + + + + + + + + + + + {/* CHANGE: Update icon. */} + + + + + + + +); ``` -Within the tab bar (``), change the label to “Photos” and the `ellipse` icon to `images` for the middle tab button: - -```tsx - - - Photos - -``` - -:::note -In Ionic React, icons are imported individually from `ionicons/icons` and set to the icon prop. -::: - That’s just the start of all the cool things we can do with Ionic. Up next, implement camera taking functionality on the web, then build it for iOS and Android. diff --git a/versioned_docs/version-v7/react/your-first-app/2-taking-photos.md b/versioned_docs/version-v7/react/your-first-app/2-taking-photos.md index 23f63ef616..305970a351 100644 --- a/versioned_docs/version-v7/react/your-first-app/2-taking-photos.md +++ b/versioned_docs/version-v7/react/your-first-app/2-taking-photos.md @@ -4,41 +4,35 @@ sidebar_label: Taking Photos --- - Take Photos From The Camera on React Apps - Ionic Documentation + Take Photos with Camera API for iOS, Android & Web with React | Ionic Capacitor Camera -Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](https://capacitorjs.com/docs/apis/camera). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). +# Taking Photos with the Camera -To do so, we will create our own custom React hook that will manage the photos for the gallery. +Now for the fun part - adding the ability to take photos with the device’s camera using the Capacitor [Camera API](../../native/camera.md). We’ll begin with building it for the web, then make some small tweaks to make it work on mobile (iOS and Android). -:::note -If you are not familiar with React Hooks, [Introducing React Hooks](https://reactjs.org/docs/hooks-intro.html) from the official React docs is a good resource to start with. -::: +## Photo Gallery Hook + +We will create a [custom React hook](https://react.dev/learn/reusing-logic-with-custom-hooks#extracting-your-own-custom-hook-from-a-component) to manage the photos for the gallery. Create a new file at `src/hooks/usePhotoGallery.ts` and open it up. -A custom hook is just a function that uses other React hooks. And that's what we will be doing! We will start by importing the various hooks and utilities we will be using from React core, the Ionic React Hooks project, and Capacitor: +Next, define a new method, `usePhotoGallery()`, that will contain the core logic to take a device photo and save it to the filesystem. Let’s start by opening the device camera. -```tsx +```ts import { useState, useEffect } from 'react'; -import { isPlatform } from '@ionic/react'; - import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; import { Filesystem, Directory } from '@capacitor/filesystem'; import { Preferences } from '@capacitor/preferences'; -import { Capacitor } from '@capacitor/core'; -``` -Next, create a function named usePhotoGallery: - -```tsx export function usePhotoGallery() { - const takePhoto = async () => { - const photo = await Camera.getPhoto({ + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ resultType: CameraResultType.Uri, source: CameraSource.Camera, quality: 100, @@ -46,101 +40,229 @@ export function usePhotoGallery() { }; return { - takePhoto, + addNewToGallery, }; } ``` -Our `usePhotoGallery` hook exposes a method called takePhoto, which in turn calls into Capacitor's getPhoto method. - Notice the magic here: there's no platform-specific code (web, iOS, or Android)! The Capacitor Camera plugin abstracts that away for us, leaving just one method call - `getPhoto()` - that will open up the device's camera and allow us to take photos. -The last step we need to take is to use the new hook from the Tab2 page. Go back to Tab2.tsx and import the hook: +Next, in `Tab2.tsx`, import the `usePhotoGallery()` method and destructure it to call its `addNewToGallery()` method. ```tsx +import { camera, trash, close } from 'ionicons/icons'; +import { + IonContent, + IonHeader, + IonPage, + IonTitle, + IonToolbar, + IonFab, + IonFabButton, + IonIcon, + IonGrid, + IonRow, + IonCol, + IonImg, + IonActionSheet, +} from '@ionic/react'; +// CHANGE: Add `usePhotoGallery` import. import { usePhotoGallery } from '../hooks/usePhotoGallery'; -``` +import './Tab2.css'; -And right before the return statement in the functional component, get access to the `takePhoto` method by using the hook: - -```tsx const Tab2: React.FC = () => { - const { takePhoto } = usePhotoGallery(); + // CHANGE: Destructure `addNewToGallery()` from `usePhotoGallery()`. + const { addNewToGallery } = usePhotoGallery(); + + return ( + + + + Photo Gallery + + + + + + Photo Gallery + + + + + {/* CHANGE: Add a click event listener to the floating action button. */} + addNewToGallery()}> + + + + + + ); +}; - // snip - rest of code +export default Tab2; ``` -Save the file, and if you’re not already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! +If it's not running already, restart the development server in your browser by running `ionic serve`. On the Photo Gallery tab, click the Camera button. If your computer has a webcam of any sort, a modal window appears. Take a selfie! ![A photo gallery app displaying a webcam selfie.](/img/guides/first-app-cap-ng/camera-web.png 'Webcam Selfie in Photo Gallery') _(Your selfie is probably much better than mine)_ -After taking a photo, it disappears. We still need to display it within our app and save it for future access. +After taking a photo, it disappears right away. We need to display it within our app and save it for future access. ## Displaying Photos -First we will create a new type to define our Photo, which will hold specific metadata. Add the following UserPhoto interface to the `usePhotoGallery.ts` file, somewhere outside of the main function: +Return to `usePhotoGallery.ts`. -```tsx +Outside of the `usePhotoGallery()` method definition (the very bottom of the file), create a new interface, `UserPhoto`, to hold our photo metadata. + +```ts +export function usePhotoGallery { + // Same old code from before. +} + +// CHANGE: Add the `UserPhoto` interface. export interface UserPhoto { filepath: string; webviewPath?: string; } ``` -Back at the top of the function (right after the call to `usePhotoGallery`, we will define a state variable to store the array of each photo captured with the Camera. +Above the `addNewToGallery()` method, define an array of `UserPhoto`, which will contain a reference to each photo captured with the Camera. Make it a state variable using React's [useState hook](https://react.dev/reference/react/useState). -```tsx -const [photos, setPhotos] = useState([]); +```ts +export function usePhotoGallery { + // CHANGE: Add the `photos` array. + const [photos, setPhotos] = useState([]); + + // Same old code from before. +} ``` -When the camera is done taking a picture, the resulting Photo returned from Capacitor will be stored in the `photo` variable. We want to create a new photo object and add it to the photos state array. We make sure we don't accidentally mutate the current photos array by making a new array, and then call `setPhotos` to store the array into state. Update the `takePhoto` method and add this code after the getPhoto call: +Over in the `addNewToGallery()` method, add the newly captured photo to the beginning of the `photos` array. Then, update the `userPhotoGallery()` return statement with the `photos` array. -```tsx -const fileName = Date.now() + '.jpeg'; -const newPhotos = [ - { - filepath: fileName, - webviewPath: photo.webPath, - }, - ...photos, -]; -setPhotos(newPhotos); -``` +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); -Next, let's expose the photos array from our hook. Update the return statement to include the photos: + // CHANGE: Create the `fileName` with current timestamp. + const fileName = Date.now() + '.jpeg'; + // CHANGE: Create `savedImageFile` matching `UserPhoto` interface. + const savedImageFile = [ + { + filepath: fileName, + webviewPath: capturedPhoto.webPath, + }, + ...photos, + ]; + + // CHANGE: Update the `photos` array with the new photo. + setPhotos(savedImageFile); + }; -```tsx -return { - photos, - takePhoto, -}; + return { + addNewToGallery, + // CHANGE: Update return statement to include `photos` array. + photos, + }; +} ``` -And back in the Tab2 component, get access to the photos: +`usePhotoGallery.ts` should now look like this: -```tsx -const { photos, takePhoto } = usePhotoGallery(); +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + const savedImageFile = [ + { + filepath: fileName, + webviewPath: capturedPhoto.webPath, + }, + ...photos, + ]; + + setPhotos(savedImageFile); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -With the photo(s) stored into the main array we can display the images on the screen. Add a [Grid component](https://ionicframework.com/docs/api/grid) so that each photo will display nicely as photos are added to the gallery, and loop through each photo in the Photos array, adding an Image component (``) for each. Point the `src` (source) to the photo’s path: +Next, switch to `Tab2.tsx` to display the images. We'll add a [Grid component](../../api/grid.md) to ensure the photos display neatly as they're added to the gallery. Inside the grid, loop through each photo in the `UserPhoto`'s `photos` array. For each item, add an [Image component](../../api/img.md) and set its `src` property to the photo's path. ```tsx - - - - {photos.map((photo, index) => ( - - - - ))} - - - - +const Tab2: React.FC = () => { + // CHANGE: Add `photos` array to destructure from `usePhotoGallery()`. + const { photos, addNewToGallery } = usePhotoGallery(); + + return ( + + + + Photo Gallery + + + + + + Photo Gallery + + + + {/* CHANGE: Add a grid component to display the photos. */} + + + {/* CHANGE: Create a new column and image component for each photo. */} + {photos.map((photo) => ( + + + + ))} + + + + + addNewToGallery()}> + + + + + + ); +}; ``` -Save all files. Within the web browser, click the Camera button and take another photo. This time, the photo is displayed in the Photo Gallery! +Within the web browser, click the camera button and take another photo. This time, the photo is displayed in the Photo Gallery! Up next, we’ll add support for saving the photos to the filesystem, so they can be retrieved and displayed in our app at a later time. diff --git a/versioned_docs/version-v7/react/your-first-app/3-saving-photos.md b/versioned_docs/version-v7/react/your-first-app/3-saving-photos.md index eae9a61df7..a1ec2b70c8 100644 --- a/versioned_docs/version-v7/react/your-first-app/3-saving-photos.md +++ b/versioned_docs/version-v7/react/your-first-app/3-saving-photos.md @@ -1,25 +1,124 @@ --- +title: Saving Photos to the Filesystem sidebar_label: Saving Photos --- + + Saving Photos to the Filesystem with React | Ionic Capacitor Camera + + + # Saving Photos to the Filesystem -We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be lost. +We’re now able to take multiple photos and display them in a photo gallery on the second tab of our app. These photos, however, are not currently being stored permanently, so when the app is closed, they will be deleted. ## Filesystem API -Fortunately, saving them to the filesystem only takes a few steps. Begin by opening the `usePhotoGallery` hook (`src/hooks/usePhotoGallery.ts`), and get access to the `writeFile` method from the `Filesystem` class: +Fortunately, saving them to the filesystem only takes a few steps. Begin by creating a new class method, `savePicture()`, in the `usePhotoGallery()` method. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -:::note -We will use the `writeFile` method initially, but we will use the others coming up shortly, so we'll go ahead and import them now. -::: +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Add the `savePicture()` method. + const savePicture = async (photo: Photo, fileName: string): Promise => { + return { + filepath: 'soon...', + webviewPath: 'soon...', + }; + }; -Next, create a couple of new functions in `usePhotoGallery`: + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +We can use this new method immediately in `addNewToGallery()`. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -```tsx export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + // CHANGE: Add `savedImageFile`. + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + // CHANGE: Update state with new photo. + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + }; + + // CHANGE: Add `savePicture()` method. + const savePicture = async (photo: Photo, fileName: string): Promise => { + return { + filepath: 'soon...', + webviewPath: 'soon...', + }; + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +We'll use the Capacitor [Filesystem API](../../native/filesystem.md) to save the photo. First, convert the photo to base64 format. + +Then, pass the data to the Filesystem's `writeFile` method. Recall that we display photos by setting the image's source path (`src`) to the `webviewPath` property. So, set the `webviewPath` and return the new `Photo` object. + +For now, create a new helper method, `convertBlobToBase64()`, to implement the necessary logic for running on the web. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; + +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Update the `savePicture()` method. const savePicture = async (photo: Photo, fileName: string): Promise => { - const base64Data = await base64FromPath(photo.webPath!); + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, @@ -33,49 +132,101 @@ export function usePhotoGallery() { webviewPath: photo.webPath, }; }; -} -export async function base64FromPath(path: string): Promise { - const response = await fetch(path); - const blob = await response.blob(); - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onerror = reject; - reader.onload = () => { - if (typeof reader.result === 'string') { + // CHANGE: Add `convertBlobToBase64()` method. + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { resolve(reader.result); - } else { - reject('method did not return a string'); - } - }; - reader.readAsDataURL(blob); - }); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; } ``` -:::note -The base64FromPath method is a helper util that downloads a file from the supplied path and returns a base64 representation of that file. -::: +`usePhotoGallery.ts` should now look like this: -We pass in the `photo` object, which represents the newly captured device photo, as well as the fileName, which will provide a path for the file to be stored to. +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -Next we use the Capacitor [Filesystem API](https://capacitorjs.com/docs/apis/filesystem) to save the photo to the filesystem. We start by converting the photo to base64 format, then feed the data to the Filesystem’s `writeFile` function. +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); -Last, call `savePicture` and pass in the photo object and filename directly underneath the call to `setPhotos` in the `takePhoto` method. Here is the full method: + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); -```tsx -const takePhoto = async () => { - const photo = await Camera.getPhoto({ - resultType: CameraResultType.Uri, - source: CameraSource.Camera, - quality: 100, - }); + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + }; - const fileName = Date.now() + '.jpeg'; - const savedFileImage = await savePicture(photo, fileName); - const newPhotos = [savedFileImage, ...photos]; - setPhotos(newPhotos); -}; + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. +Obtaining the camera photo as base64 format on the web appears to be a bit trickier than on mobile. In reality, we’re just using built-in web APIs: [fetch()](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) as a neat way to read the file into blob format, then FileReader’s [readAsDataURL()](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) to convert the photo blob to base64. + +There we go! Each time a new photo is taken, it’s now automatically saved to the filesystem. Next up, we'll load and display our saved images. diff --git a/versioned_docs/version-v7/react/your-first-app/4-loading-photos.md b/versioned_docs/version-v7/react/your-first-app/4-loading-photos.md index 75c1cb8e90..050fbc9e51 100644 --- a/versioned_docs/version-v7/react/your-first-app/4-loading-photos.md +++ b/versioned_docs/version-v7/react/your-first-app/4-loading-photos.md @@ -4,62 +4,199 @@ sidebar_label: Loading Photos --- - Loading Photos from the Filesystem Using A Key-Value Store + Loading Photos from the Filesystem with React | Ionic Capacitor Camera +# Loading Photos from the Filesystem + We’ve implemented photo taking and saving to the filesystem. There’s one last piece of functionality missing: the photos are stored in the filesystem, but we need a way to save pointers to each file so that they can be displayed again in the photo gallery. -Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](https://capacitorjs.com/docs/apis/preferences) to store our array of Photos in a key-value store. +Fortunately, this is easy: we’ll leverage the Capacitor [Preferences API](../../native/preferences.md) to store our array of Photos in a key-value store. ## Preferences API -Begin by defining a constant variable that will act as the key for the store before the `usePhotoGallery` function definition in `src/hooks/usePhotoGallery.ts`: +Open `usePhotoGallery.ts` and begin by defining a constant variable that will act as the key for the store. + +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + // CHANGE: Add a key for photo storage. + const PHOTO_STORAGE = 'photos'; -```tsx -const PHOTO_STORAGE = 'photos'; -export function usePhotoGallery() {} + // Same old code from before. +} ``` -Then, use the `Storage` class to get access to the get and set methods for reading and writing to device storage: +Next, at the end of the `addNewToGallery()` method, add a call to the `Preferences.set()` method to save the `photos` array. By adding it here, the `photos` array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. -At the end of the `takePhoto` function, add a call to `Preferences.set()` to save the Photos array. By adding it here, the Photos array is stored each time a new photo is taken. This way, it doesn’t matter when the app user closes or switches to a different app - all photo data is saved. +```ts +const addNewToGallery = async () => { + // Same old code from before. -```tsx -Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + // CHANGE: Add method to cache all photo data for future retrieval. + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); +}; ``` -With the photo array data saved, we will create a method that will retrieve the data when the hook loads. We will do so by using React's `useEffect` hook. Insert this above the `takePhoto` declaration. Here is the code, and we will break it down: - -```tsx -useEffect(() => { - const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; - - for (let photo of photosInPreferences) { - const file = await Filesystem.readFile({ - path: photo.filepath, - directory: Directory.Data, - }); - // Web platform only: Load the photo as base64 data - photo.webviewPath = `data:image/jpeg;base64,${file.data}`; - } - setPhotos(photosInPreferences); - }; - loadSaved(); -}, []); +With the photo array data saved, create a new method in the `usePhotoGallery()` called `loadSaved()` that can retrieve the photo data. We use the same key to retrieve the `photos` array in JSON format, then parse it into an array. + +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + // CHANGE: Add useEffect hook. + useEffect(() => { + // CHANGE: Add `loadSaved()` method. + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + }; + + loadSaved(); + }, []); + + // Same old code from before. +} ``` -This seems a bit scary at first, so let's walk through it, first by looking at the second parameter we pass into the hook: the dependency array `[]`. +The second parameter, the empty dependency array (`[]`), is what tells React to only run the function once. Normally, [useEffect hooks](https://react.dev/reference/react/useEffect) run after every render, but passing an empty array prevents it from running again because none of the dependencies, the values the hook relies on, will ever change. + +On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the `Filesystem`, displaying them automatically. On the web, however, we must read each image from the `Filesystem` into base64 format, using a new `base64` property on the `Photo` object. This is because the `Filesystem` API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Add the following code to complete the `loadSaved()` method. + +```ts +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + useEffect(() => { + // CHANGE: Update `loadSaved()` method. + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // CHANGE: Display the photo by reading into base64 format. + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + setPhotos(photosInPreferences); + }; + + loadSaved(); + }, []); + + // Same old code from before. +} +``` + +`usePhotoGallery.ts` should now look like this: + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; -The useEffect hook, by default, gets called each time a component renders, unless, we pass in a dependency array. In that case, it will only run when a dependency gets updated. In our case we only want it to be called once. By passing in an empty array, which will not be changed, we can prevent the hook from being called multiple times. +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + useEffect(() => { + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + + setPhotos(photosInPreferences); + }; + + loadSaved(); + }, []); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + const base64Data = (await convertBlobToBase64(blob)) as string; + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + }; + + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` -The first parameter to `useEffect` is the function that will be called by the effect. We pass in an anonymous arrow function, and inside of it we define another asynchronous method and then immediately call this. We have to call the async function from within the hook as the hook callback can't be asynchronous itself. +:::note +If you're seeing broken image links or missing photos after following these steps, you may need to open your browser's dev tools and clear both [localStorage](https://developer.chrome.com/docs/devtools/storage/localstorage) and [IndexedDB](https://developer.chrome.com/docs/devtools/storage/indexeddb). -On mobile (coming up next!), we can directly set the source of an image tag - `` - to each photo file on the Filesystem, displaying them automatically. On the web, however, we must read each image from the Filesystem into base64 format, because the Filesystem API stores them in base64 within [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. +In localStorage, look for domain `http://localhost:8100` and key `CapacitorStorage.photos`. In IndexedDB, find a store called "FileStorage". Your photos will have a key like `/DATA/123456789012.jpeg`. +::: That’s it! We’ve built a complete Photo Gallery feature in our Ionic app that works on the web. Next up, we’ll transform it into a mobile app for iOS and Android! diff --git a/versioned_docs/version-v7/react/your-first-app/5-adding-mobile.md b/versioned_docs/version-v7/react/your-first-app/5-adding-mobile.md index fa1799da9d..1b2915d2d1 100644 --- a/versioned_docs/version-v7/react/your-first-app/5-adding-mobile.md +++ b/versioned_docs/version-v7/react/your-first-app/5-adding-mobile.md @@ -1,38 +1,73 @@ --- +title: Adding Mobile sidebar_label: Adding Mobile --- + + Adding Mobile Support with React | Ionic Capacitor Camera + + + # Adding Mobile Our photo gallery app won’t be complete until it runs on iOS, Android, and the web - all using one codebase. All it takes is some small logic changes to support mobile platforms, installing some native tooling, then running the app on a device. Let’s go! +## Import Platform API + Let’s start with making some small code changes - then our app will “just work” when we deploy it to a device. +Import the Ionic [Platform API](../platform.md) into `usePhotoGallery.ts`, which is used to retrieve information about the current device. In this case, it’s useful for selecting which code to execute based on the platform the app is running on (web or mobile). + +Add `isPlatform` to the imports at the top of the file to use the `isPlatform` method. `Capacitor` is also imported to help with file paths on mobile devices. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +// CHANGE: Add imports. +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; + +// Same old code from before. +``` + ## Platform-specific Logic -First, we’ll update the photo saving functionality to support mobile. In the `savePicture` function, check which platform the app is running on. If it’s “hybrid” (Capacitor or Cordova, the two native runtimes), then read the photo file into base64 format using the `readFile` method. Also, return the complete file path to the photo using the Filesystem API. When setting the `webviewPath`, use the special `Capacitor.convertFileSrc` method ([details here](https://ionicframework.com/docs/core-concepts/webview#file-protocol)). Otherwise, use the same logic as before when running the app on the web. +First, we’ll update the photo saving functionality to support mobile. In the `savePicture()` method, check which platform the app is running on. If it’s “hybrid” (Capacitor, the native runtime), then read the photo file into base64 format using the `Filesystem`'s' `readFile()` method. Otherwise, use the same logic as before when running the app on the web. + +Update `savePicture()` to look like the following: -```tsx +```ts +// CHANGE: Update the `savePicture()` method. const savePicture = async (photo: Photo, fileName: string): Promise => { let base64Data: string | Blob; - // "hybrid" will detect Cordova or Capacitor; + // CHANGE: Add platform check. + // "hybrid" will detect mobile - iOS or Android if (isPlatform('hybrid')) { const file = await Filesystem.readFile({ path: photo.path!, }); base64Data = file.data; } else { - base64Data = await base64FromPath(photo.webPath!); + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; } + const savedFile = await Filesystem.writeFile({ path: fileName, data: base64Data, directory: Directory.Data, }); + // CHANGE: Add platform check. if (isPlatform('hybrid')) { // Display the new image by rewriting the 'file://' path to HTTP - // Details: https://ionicframework.com/docs/building/webview#file-protocol return { filepath: savedFile.uri, webviewPath: Capacitor.convertFileSrc(savedFile.uri), @@ -48,26 +83,145 @@ const savePicture = async (photo: Photo, fileName: string): Promise = }; ``` -Next, add a new bit of logic in the `loadSaved` function. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved` function inside of `useEffect` to: +Next, add a new bit of logic in the `loadSaved()` method. On mobile, we can directly point to each photo file on the Filesystem and display them automatically. On the web, however, we must read each image from the Filesystem into base64 format. This is because the Filesystem API uses [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) under the hood. Update the `loadSaved()` method: -```tsx +```ts +// CHANGE: Update `loadSaved` method. const loadSaved = async () => { - const { value } = await Preferences.get({ key: PHOTO_STORAGE }); + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; - const photosInPreferences = (value ? JSON.parse(value) : []) as UserPhoto[]; + // CHANGE: Add platform check. // If running on the web... if (!isPlatform('hybrid')) { - for (let photo of photosInPreferences) { + for (const photo of photosInPreferences) { const file = await Filesystem.readFile({ path: photo.filepath, directory: Directory.Data, }); - // Web platform only: Load the photo as base64 data photo.webviewPath = `data:image/jpeg;base64,${file.data}`; } } + setPhotos(photosInPreferences); }; ``` -Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. Next up, the part you’ve been waiting for - deploying the app to a device. +Our Photo Gallery now consists of one codebase that runs on the web, Android, and iOS. + +`usePhotoGallery.ts` should now look like this: + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; + +export function usePhotoGallery() { + const [photos, setPhotos] = useState([]); + + const PHOTO_STORAGE = 'photos'; + + useEffect(() => { + const loadSaved = async () => { + const { value: photoList } = await Preferences.get({ key: PHOTO_STORAGE }); + const photosInPreferences = (photoList ? JSON.parse(photoList) : []) as UserPhoto[]; + + // If running on the web... + if (!isPlatform('hybrid')) { + for (const photo of photosInPreferences) { + const file = await Filesystem.readFile({ + path: photo.filepath, + directory: Directory.Data, + }); + photo.webviewPath = `data:image/jpeg;base64,${file.data}`; + } + } + + setPhotos(photosInPreferences); + }; + + loadSaved(); + }, []); + + const addNewToGallery = async () => { + // Take a photo + const capturedPhoto = await Camera.getPhoto({ + resultType: CameraResultType.Uri, + source: CameraSource.Camera, + quality: 100, + }); + + const fileName = Date.now() + '.jpeg'; + // Save the picture and add it to photo collection + const savedImageFile = await savePicture(capturedPhoto, fileName); + + const newPhotos = [savedImageFile, ...photos]; + setPhotos(newPhotos); + + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + }; + + const savePicture = async (photo: Photo, fileName: string): Promise => { + let base64Data: string | Blob; + // "hybrid" will detect mobile - iOS or Android + if (isPlatform('hybrid')) { + const file = await Filesystem.readFile({ + path: photo.path!, + }); + base64Data = file.data; + } else { + // Fetch the photo, read as a blob, then convert to base64 format + const response = await fetch(photo.webPath!); + const blob = await response.blob(); + base64Data = (await convertBlobToBase64(blob)) as string; + } + + const savedFile = await Filesystem.writeFile({ + path: fileName, + data: base64Data, + directory: Directory.Data, + }); + + if (isPlatform('hybrid')) { + // Display the new image by rewriting the 'file://' path to HTTP + return { + filepath: savedFile.uri, + webviewPath: Capacitor.convertFileSrc(savedFile.uri), + }; + } else { + // Use webPath to display the new image instead of base64 since it's + // already loaded into memory + return { + filepath: fileName, + webviewPath: photo.webPath, + }; + } + }; + + const convertBlobToBase64 = (blob: Blob) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onerror = reject; + reader.onload = () => { + resolve(reader.result); + }; + reader.readAsDataURL(blob); + }); + }; + + return { + addNewToGallery, + photos, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} +``` + +Next up, the part you’ve been waiting for - deploying the app to a device. diff --git a/versioned_docs/version-v7/react/your-first-app/6-deploying-mobile.md b/versioned_docs/version-v7/react/your-first-app/6-deploying-mobile.md index da780fa76a..d6101c33d0 100644 --- a/versioned_docs/version-v7/react/your-first-app/6-deploying-mobile.md +++ b/versioned_docs/version-v7/react/your-first-app/6-deploying-mobile.md @@ -1,10 +1,23 @@ --- +title: Deploying to iOS and Android sidebar_label: Deploying Mobile --- + + Adding Mobile Support with React | Ionic Capacitor Camera + + + # Deploying to iOS and Android -Since we added Capacitor to our project when it was first created, there’s only a handful of steps remaining until the Photo Gallery app is on our device! Remember, you can find the complete source code for this app [here](https://github.com/ionic-team/photo-gallery-capacitor-react). +Since we added Capacitor to our project when it was first created, there’s only a handful of steps remaining until the Photo Gallery app is on our device! + +:::note +Remember, you can find the complete source code for this app [here](https://github.com/ionic-team/photo-gallery-capacitor-react). +::: ## Capacitor Setup @@ -19,8 +32,8 @@ ionic build Next, create both the iOS and Android projects: ```shell -$ ionic cap add ios -$ ionic cap add android +ionic cap add ios +ionic cap add android ``` Both android and ios folders at the root of the project are created. These are entirely standalone native projects that should be considered part of your Ionic app (i.e., check them into source control, edit them using their native tooling, etc.). @@ -43,7 +56,7 @@ ionic cap sync To build an iOS app, you’ll need a Mac computer. ::: -Capacitor iOS apps are configured and managed through Xcode (Apple’s iOS/Mac IDE), with dependencies managed by CocoaPods. Before running this app on an iOS device, there's a couple of steps to complete. +Capacitor iOS apps are configured and managed through Xcode (Apple’s iOS/Mac IDE), with dependencies managed by [CocoaPods](https://cocoapods.org/). Before running this app on an iOS device, there's a couple of steps to complete. First, run the Capacitor `open` command, which opens the native iOS project in Xcode: @@ -51,7 +64,7 @@ First, run the Capacitor `open` command, which opens the native iOS project in X ionic cap open ios ``` -In order for some native plugins to work, user permissions must be configured. In our photo gallery app, this includes the Camera plugin: iOS displays a modal dialog automatically after the first time that `Camera.getPhoto()` is called, prompting the user to allow the app to use the Camera. The permission that drives this is labeled “Privacy - Camera Usage.” To set it, the `Info.plist` file must be modified ([more details here](https://capacitorjs.com/docs/ios/configuration)). To access it, click "Info," then expand "Custom iOS Target Properties." +In order for some native plugins to work, user permissions must be configured. In our photo gallery app, this includes the Camera plugin: iOS displays a modal dialog automatically after the first time that `Camera.getPhoto()` is called, prompting the user to allow the app to use the Camera. The permission that drives this is labeled "Privacy - Camera Usage." To set it, the `Info.plist` file must be modified ([more details here](https://capacitorjs.com/docs/ios/configuration)). To access it, click "Info," then expand "Custom iOS Target Properties." ![The Info.plist file in Xcode showing the NSCameraUsageDescription key added for camera access.](/img/guides/first-app-cap-ng/xcode-info-plist.png 'Xcode Info.plist Configuration') diff --git a/versioned_docs/version-v7/react/your-first-app/7-live-reload.md b/versioned_docs/version-v7/react/your-first-app/7-live-reload.md index 15d4f643f2..3652b25f53 100644 --- a/versioned_docs/version-v7/react/your-first-app/7-live-reload.md +++ b/versioned_docs/version-v7/react/your-first-app/7-live-reload.md @@ -1,120 +1,162 @@ --- +title: Rapid App Development with Live Reload sidebar_label: Live Reload --- + + Rapid App Development with Live Reload with React | Ionic Capacitor Camera + + + # Rapid App Development with Live Reload So far, we’ve seen how easy it is to develop a cross-platform app that works everywhere. The development experience is pretty quick, but what if I told you there was a way to go faster? -We can use the Ionic CLI’s [Live Reload functionality](https://ionicframework.com/docs/cli/livereload) to boost our productivity when building Ionic apps. When active, Live Reload will reload the browser and/or WebView when changes in the app are detected. +We can use the Ionic CLI’s [Live Reload functionality](../../cli/livereload.md) to boost our productivity when building Ionic apps. When active, Live Reload will reload the browser and/or WebView when changes in the app are detected. ## Live Reload Remember `ionic serve`? That was Live Reload working in the browser, allowing us to iterate quickly. -We can also use it when developing on iOS and Android devices. This is particularly useful when writing code that interacts with native plugins. Since we need to run native plugin code on a device in order to verify that it works, having a way to quickly write code, build and deploy it, then test it is crucial to keeping up our development speed. +We can also use it when developing on iOS and Android devices. This is particularly useful when writing code that interacts with native plugins - we must run it on a device to verify that it works. Therefore, being able to quickly write, build, test, and deploy code is crucial to keeping up our development speed. Let’s use Live Reload to implement photo deletion, the missing piece of our Photo Gallery feature. Select your platform of choice (iOS or Android) and connect a device to your computer. Next, run either command in a terminal, based on your chosen platform: ```shell -$ ionic cap run ios -l --external +ionic cap run ios -l --external -$ ionic cap run android -l --external +ionic cap run android -l --external ``` The Live Reload server will start up, and the native IDE of choice will open if not opened already. Within the IDE, click the Play button to launch the app onto your device. ## Deleting Photos -With Live Reload running and the app open on your device, let’s implement photo deletion functionality. In your code editor (not Android Studio or Xcode), open `Tab2.tsx` then import `useState` from React and `UserPhoto` from the `usePhotoGallery` hook: - -```tsx -import React, { useState } from 'react'; -import { usePhotoGallery, UserPhoto } from '../hooks/usePhotoGallery'; -// other imports -``` - -Next, reference the `deletePhoto` function, which we'll create soon: - -```tsx -const { photos, takePhoto, deletePhoto } = usePhotoGallery(); -``` - -Next, add a state value to store information about the photo to delete: - -```tsx -const [photoToDelete, setPhotoToDelete] = useState(); -``` - -When a user clicks on an image, we will show the action sheet by changing the state value to the photo. Update the `` element to: - -```tsx - setPhotoToDelete(photo)} src={photo.webviewPath} /> -``` - -Next, add an [IonActionSheet](https://ionicframework.com/docs/api/action-sheet) dialog with the option to either delete the selected photo or cancel (close) the dialog. We will set the isOpen property based on if photoToDelete has a value or not. - -In the JSX, put the following component before the closing `` tag. - -```tsx - { - if (photoToDelete) { - deletePhoto(photoToDelete); - setPhotoToDelete(undefined); - } - }, - }, - { - text: 'Cancel', - icon: close, - role: 'cancel', - }, - ]} - onDidDismiss={() => setPhotoToDelete(undefined)} -/> +With Live Reload running and the app open on your device, let’s implement photo deletion functionality. + +In `usePhotoGallery.ts`, add the `deletePhoto()` method. The selected photo is removed from the `photos` array first. Then, we delete the actual photo file itself using the Filesystem API. + +```ts +import { useState, useEffect } from 'react'; +import { Camera, CameraResultType, CameraSource, Photo } from '@capacitor/camera'; +import { Filesystem, Directory } from '@capacitor/filesystem'; +import { Preferences } from '@capacitor/preferences'; +import { isPlatform } from '@ionic/react'; +import { Capacitor } from '@capacitor/core'; + +export function usePhotoGallery() { + // Same old code from before. + + // CHANGE: Add `deletePhoto()` method. + const deletePhoto = async (photo: UserPhoto) => { + // Remove this photo from the Photos reference data array + const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); + + // Update photos array cache by overwriting the existing photo array + Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); + + // Delete photo file from filesystem + const filename = photo.filepath.substr(photo.filepath.lastIndexOf('/') + 1); + await Filesystem.deleteFile({ + path: filename, + directory: Directory.Data, + }); + + setPhotos(newPhotos); + }; + + return { + photos, + addNewToGallery, + // CHANGE: Add `deletePhoto()` to the return statement. + deletePhoto, + }; +} + +export interface UserPhoto { + filepath: string; + webviewPath?: string; +} ``` -Above, we added two options: `Delete` that calls `deletePhoto` function (to be added next) and `Cancel`, which when given the role of “cancel” will automatically close the action sheet. It's also important to set the onDidDismiss function and set our photoToDelete back to undefined when the modal goes away. That way, when another image is clicked, the action sheet notices the change in the value of photoToDelete. - -Next, we need to implement the deletePhoto method that will come from the `usePhotoGallery` hook. Open the file and paste in the following function in the hook: +Next, in `Tab2.tsx`, implement the `IonActionSheet` component. We're adding two options: "Delete", which calls `usePhotoGallery.deletePicture()`, and "Cancel". The cancel button will automatically closes the action sheet when assigned the "cancel" role. ```tsx -const deletePhoto = async (photo: UserPhoto) => { - // Remove this photo from the Photos reference data array - const newPhotos = photos.filter((p) => p.filepath !== photo.filepath); - - // Update photos array cache by overwriting the existing photo array - Preferences.set({ key: PHOTO_STORAGE, value: JSON.stringify(newPhotos) }); - - // delete photo file from filesystem - const filename = photo.filepath.substring(photo.filepath.lastIndexOf('/') + 1); - await Filesystem.deleteFile({ - path: filename, - directory: Directory.Data, - }); - setPhotos(newPhotos); +// Same old code from before. +// change: Add React import. +import { useState } from 'react'; +// CHANGE: Add `UserPhoto` type import. +import type { UserPhoto } from '../hooks/usePhotoGallery'; + +const Tab2: React.FC = () => { + // CHANGE: Add `deletePhoto()` method. + const { photos, addNewToGallery, deletePhoto } = usePhotoGallery(); + // CHANGE: Add state for the photo to delete. + const [photoToDelete, setPhotoToDelete] = useState(); + + return ( + + + {/* Same old code from before. */} + + + addNewToGallery()}> + + + + + {/* CHANGE: Add action sheet for deleting photos. */} + { + if (photoToDelete) { + deletePhoto(photoToDelete); + setPhotoToDelete(undefined); + } + }, + }, + { + text: 'Cancel', + icon: close, + role: 'cancel', + handler: () => { + // Nothing to do, action sheet is automatically closed + }, + }, + ]} + onDidDismiss={() => setPhotoToDelete(undefined)} + > + + + ); }; ``` -The selected photo is removed from the Photos array first. Then, we use the Capacitor Preferences API to update the cached version of the Photos array. Finally, we delete the actual photo file itself using the Filesystem API. - -Make sure to return the `deletePhoto` function so it is as a part of the hook API that we expose: +Add a click handler to the `` element. When the app user taps on a photo in our gallery, we’ll display an [Action Sheet](../../api/action-sheet.md) dialog with the option to either delete the selected photo or cancel (close) the dialog. ```tsx -return { - photos, - takePhoto, - deletePhoto, -}; + + + {photos.map((photo) => ( + + {/* CHANGE: Add a click event listener to each image. */} + setPhotoToDelete(photo)} /> + + ))} + + ``` -Save this file, then tap on a photo again and choose the “Delete” option. This time, the photo is deleted! Implemented much faster using Live Reload. 💪 +Remember that removing the photo from the `photos` array triggers the `cachePhotos` method for us automatically. + +Tap on a photo again and choose the “Delete” option. The photo is deleted! Implemented much faster using Live Reload. 💪 In the final portion of this tutorial, we’ll walk you through the basics of the Appflow product used to build and deploy your application to users' devices. diff --git a/versioned_docs/version-v7/react/your-first-app/8-distribute.md b/versioned_docs/version-v7/react/your-first-app/8-distribute.md index 9fb5290437..1ebffeceda 100644 --- a/versioned_docs/version-v7/react/your-first-app/8-distribute.md +++ b/versioned_docs/version-v7/react/your-first-app/8-distribute.md @@ -1,7 +1,16 @@ --- +title: Build and Deploy your App sidebar_label: Distribute --- + + Build and Deploy your App with React | Ionic Capacitor Camera + + + # Build and Deploy your App Now that you have built your first app, you are going to want to get it distributed so everyone can start using it. The mechanics of building and deploying your application can be quite cumbersome. That is where [Appflow](https://ionic.io/docs/appflow/) comes into play. Appflow allows you to effectively generate web and native builds, push out live app updates, publish your app to the app stores, and automate the whole process. The entire Quickstart guide can be found [here](https://ionic.io/docs/appflow/quickstart). @@ -63,7 +72,7 @@ To dive into more details on the steps to deploy a live update, as well as addit Next up is a native binary for your app build and deploy process. This is done via the [Ionic Package](https://ionic.io/docs/appflow/package/intro) service. First things first, you will need to create a [Package build](https://ionic.io/docs/appflow/package/builds). This can be done by clicking the `Start build` icon from the `Commits` tab or by clicking the `New build` button in the top right from the `Build > Builds` tab. Then you will select the proper commit for your build and fill in all of the several required fields and any optional fields that you want to specify. After filling in all of the information and the build begins, you can check out it's progress and review the logs if you encounter any errors. -Given a successful Package build, and iOS binary (`.ipa` or IPA) or and Android binary (`.apk` or APK) file becomes available to you. The file can subsequently be downloaded so you can install it on a device by clicking the file name in the `Artifacts` section in the right of the build detail page or clicking the `Download IPA/APK` icon on the build in the `Build > Builds` tab. +Given a successful Package build, an iOS binary (`.ipa` or IPA) or/and an Android binary (`.apk` or APK) file becomes available to you. The file can subsequently be downloaded so you can install it on a device by clicking the file name in the `Artifacts` section in the right of the build detail page or clicking the `Download IPA/APK` icon on the build in the `Build > Builds` tab. Further information regarding building native binaries can be found inside of the [Build a Native Binary](https://ionic.io/docs/appflow/quickstart/package) section inside the Appflow docs. @@ -93,8 +102,8 @@ For access to the ability to create a Native Configuration, you will need to be ## What’s Next? -Congratulations! You developed a complete cross-platform Photo Gallery app that runs on the web, iOS, and Android. Not only that, you have also then built the app and deployed it to your users devices! +Congratulations! You developed a complete cross-platform Photo Gallery app that runs on the web, iOS, and Android. Not only that, you have also then built the app and deployed it to your users' devices! -There are many paths to follow from here. Try adding another [Ionic UI component](https://ionicframework.com/docs/components) to the app, or more [native functionality](https://capacitorjs.com/docs/apis). The sky’s the limit. Once you have added another feature run the build and deploy process again through Appflow to get it out to your users. +There are many paths to follow from here. Try adding another [Ionic UI component](../../components.md) to the app, or more [native functionality](https://capacitorjs.com/docs/apis). The sky’s the limit. Once you have added another feature, run the build and deploy process again through Appflow to get it out to your users. Happy app building! 💙