Build Mobile iOS, Android, and Web Apps Using React and Ionic, Part IV

Write Once Run Anywhere Is Alive And Well In Modern JavaScript Development

This is Part IV in a series. You can find the previous Parts here III, III. In today’s story we will complete our second tab and list out our previously taken photos.

Layout In Ionic

Since this screen has more content let’s learn a bit more about layout in Ionic before continuing. If your page has multiple container elements and requires some strict rules around layout, the Ionic grid system is a built-in easy way of getting that layout. It is very similar to Bootstrap and uses grid, row, and column components for controlling your layout. The column system has up to 12 columns per screen and the columns resize based upon the size of the screen, again like Bootstrap. In addition, the columns have size tiers like small, medium, large, etc. which are based on minimum widths so up to and above a certain width that tier would apply. Note you can even have the grid system use more than 12 columns by changing the — ion-grid-columns CSS variable. There’s a lot more configurability to the grid system and more details can be found here. But for our purposes we’ll use it in a simple form within the new second tab.

You may be wondering if it’s worth it to use the Ionic grid system instead of something more established like Bootstrap or your own classes. However since the Ionic grid system is integrated with their other controls, including themes and styles, I would recommend using their system.

Let’s use this grid system in our new second tab. But first let’s create it without grids to see some of the issues grids solve. Start our second tab by creating a new page inside of the pages folder called Photos.tsx. For now create it as an empty functional component. Since we already know how to modify routes and tabs, based upon the last stories, I’ll just show you the updated App.tsx file that will display the new second tab called photos. This is what it should look like after adding Photos as the second tab. Quick note if you’re curios where the new photos icon comes from, all icons can be found and searched by going to Ionicons (the default icons for Ionic).

import React from "react";
import { Redirect, Route } from "react-router-dom";
import {
IonApp,
IonIcon,
IonLabel,
IonRouterOutlet,
IonTabBar,
IonTabButton,
IonTabs
} from "@ionic/react";
import { IonReactRouter } from "@ionic/react-router";
import { photos, camera } from "ionicons/icons";
import Tab2 from "./pages/Tab2";
import Details from "./pages/Details";
/* Core CSS required for Ionic components to work properly */
import "@ionic/react/css/core.css";
/* Basic CSS for apps built with Ionic */
import "@ionic/react/css/normalize.css";
import "@ionic/react/css/structure.css";
import "@ionic/react/css/typography.css";
/* Optional CSS utils that can be commented out */
import "@ionic/react/css/padding.css";
import "@ionic/react/css/float-elements.css";
import "@ionic/react/css/text-alignment.css";
import "@ionic/react/css/text-transformation.css";
import "@ionic/react/css/flex-utils.css";
import "@ionic/react/css/display.css";
/* Theme variables */
import "./theme/variables.css";
import Camera from "./pages/Camera";
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonTabs>
<IonRouterOutlet>
<Route path="/camera" component={Camera} exact={true} />
<Route path="/photos" component={Tab2} exact={true} />
<Route path="/photos/details" component={Details} />
<Route
path="/"
render={() => <Redirect to="/camera" />}
exact={true}
/>
</IonRouterOutlet>
<IonTabBar slot="bottom">
<IonTabButton tab="camera" href="/camera">
<IonIcon icon={camera} />
<IonLabel>Camera</IonLabel>
</IonTabButton>
<IonTabButton tab="photos" href="/photos">
<IonIcon icon={photos} />
<IonLabel>Photos</IonLabel>
</IonTabButton>
</IonTabBar>
</IonTabs>
</IonReactRouter>
</IonApp>
);
export default App;
view raw App.tsx part 4 hosted with ❤ by GitHub

Now go back to the new Photos.tsx file and add this code.

import React from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonText
} from "@ionic/react";
const Photos: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonText>test</IonText>
</IonContent>
</IonPage>
);
};
export default Photos;

If you reload the screen you will see that the text alignment is not centered and on desktop screens it starts all the way to the left.

Now let’s replace this with a grid layout so we can get automatic centering and resizing. Update the Photos.tsx file with this code.

import React from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonText
} from "@ionic/react";
const Photos: React.FC = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonText>test</IonText>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Photos;

After rebuilding you should see this for desktop screens.

The grid, along with its styling automatically centers itself across different devices. This is what it looks like on mobile devices.

Let’s review this code to understand how this centering is done. The IonGrid is the main container for grid items. The immediate child of an IonGrid must be an IonRow. And it is possible to have multiple IonRow components in an IonGrid. Inside the IonRow is an IonCol, which will contain all of our content. Now in the IonCol you can see we have a size property for medium sized screens, size-md, that we set to 6 columns. If we left it with this property alone, then this column would shift to the left as the total count needs to be 12. However since we also use the offset property, we are telling Ionic to offset this column by 3 from the left, which puts it in the horizontal middle of the screen. Let’s continue to the grid’s content. You’ll notice that the column has a CSS class “ion-text-center”, which centers the text in the IonText component. This is one of many styling and theme related shortcuts, for all Ionic components, that can be used to very quickly get desired styling effects, while maintaining adherence to the core theme. Particularly useful are the ion-padding and ion-margin CSS classes. These classes provide a theme consistent offset for your controls that’s easy to use and ensures the same spacing throughout the entire app. There are dozens of styles that can be used to adjust text, alignment, and spacing. You can see a complete list here.

Before continuing lets run the app on our iOS device or simulator. Normally you need to run Ionic build first and then the other commands that were mentioned in our previous stories, but here’s a faster shortcut.

ionic cap run ios

This single command will build, copy, and then open your XCode project. However note it does not do the sync piece. So if you’ve not done that you’ll need to do it just once, before running this command. Also note in order to sync, your Mac needs to have Cocoapods installed. Now let’s list out the photos we saved previously into the filesystem.

Bugs and Updates

The Ionic team has just released an update to their @ionic/react-hooks package and it now includes hooks for the Filesystem. So we’ll update our Camera code to use those hooks. We’ll also need to update that file as I had an infinite loop bug which was causing the code inside useEffect to run multiple times. My apologies for the issue. So let’s first update the hooks package. I had some trouble getting it to auto increase to version 0.0.6, which is the one with hooks. So I had to first update the package.json file with that version and then run below.

npm install @ionic/react-hooks

Once you do that the code in Camera.tsx should look like this.

import React, { useCallback, useEffect, useState } from "react";
import {
CameraResultType,
FilesystemDirectory,
Capacitor,
CameraSource
} from "@capacitor/core";
import { useCamera, availableFeatures } from "@ionic/react-hooks/camera";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
import { useStorage } from "@ionic/react-hooks/storage";
import moment from "moment";
import {
IonButton,
IonContent,
IonHeader,
IonToolbar,
IonTitle,
IonPage,
IonText,
IonCard,
IonCardHeader,
IonCardContent,
IonCardTitle,
IonItemDivider
} from "@ionic/react";
export const LOG_PREFIX = "[Camera] ";
const Camera: React.FC = () => {
const { readFile, writeFile, getUri } = useFilesystem();
const { photo, getPhoto } = useCamera();
const [lastWebPath, setLastWebPath] = useState("");
const [lastPhotoPath, setLastPhotoPath] = useState("");
const { get, set } = useStorage();
const triggerCamera = useCallback(async () => {
if (availableFeatures.getPhoto) {
await getPhoto({
quality: 100,
allowEditing: false,
resultType: CameraResultType.Uri,
source: CameraSource.Camera
});
}
}, [getPhoto]);
useEffect(() => {
if (photo && photo.webPath !== lastWebPath) {
setLastWebPath(photo.webPath || "");
console.log(LOG_PREFIX + "photo.webPath", photo.webPath);
console.log(LOG_PREFIX + "lastWebPath", lastWebPath);
readFile({
path: photo ? photo.path || photo.webPath || "" : ""
})
.then(photoInTemp => {
let date = moment().format("MM-DD-YY-h-mm-ss");
let fileName = date + ".jpg";
// copy file into local filesystem, as temp will eventually be deleted
writeFile({
data: photoInTemp.data,
path: fileName,
directory: FilesystemDirectory.Documents
}).then(() => {
// now we try to read the file
getUri({
directory: FilesystemDirectory.Documents,
path: fileName
}).then(async finalPhotoUri => {
const filePath = finalPhotoUri.uri;
const photoUrl = Capacitor.convertFileSrc(filePath);
if (photoUrl !== lastPhotoPath) {
setLastPhotoPath(photoUrl);
}
const imagePaths = await get("imagePaths");
const imagePathsArray = imagePaths ? JSON.parse(imagePaths) : [];
imagePathsArray.push({
fileName
});
const imagePathsArrayString = JSON.stringify(imagePathsArray);
await set("imagePaths", imagePathsArrayString);
console.log(LOG_PREFIX + "imagePaths", await get("imagePaths"));
});
});
})
.catch(err => {
console.log(err);
});
}
}, [
photo,
readFile,
writeFile,
getUri,
get,
set,
lastPhotoPath,
lastWebPath
]);
const onClick = () => {
triggerCamera();
};
if (availableFeatures.getPhoto) {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Camera</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonCard>
<IonItemDivider>
<IonCardHeader>
<IonCardTitle>My Photo</IonCardTitle>
</IonCardHeader>
</IonItemDivider>
<IonCardContent>
{lastPhotoPath && <img src={lastPhotoPath} alt="my pic" />}
<IonButton style={{ marginTop: ".75em" }} onClick={onClick}>
camera
</IonButton>
</IonCardContent>
</IonCard>
</IonContent>
</IonPage>
);
}
return (
<IonPage>
<IonContent>
<IonText>No Camera Available</IonText>
</IonContent>
</IonPage>
);
};
export default Camera;
view raw Camera bug fix hosted with ❤ by GitHub

Starting near the top, as you can see we import the filesystem hook and get useFilesystem. I’ve also created a lastWebPath state object to detect when a new picture has been taken and prevent the infinite looping issue. I’ve also forced the camera source to be CameraSource.Camera so the user will not have to choose which source object to use. Moving down to the bottom you’ll also notice that imageNames is gone now and imagePaths no longer references the photoUrl. This is because on the Photos.tsx page I am now only going to use the file name instead of the path; because converting the imagePaths from string to JSON and back was causing some issues in reading the file paths by the img tag.

Using Ionic Lifecycle Events

The Camera.tsx file is now updated and is using Capacitor to save the new picture names into local storage. Now within our Photos tab, we can use storage and filesystem hooks to retrieve the file paths and display the images into a list. Let’s update the Photos.tsx file like this.

import React, { useState } from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonLabel,
useIonViewWillEnter,
IonItemSliding,
IonItem,
IonItemOptions,
IonItemOption,
IonAvatar
} from "@ionic/react";
import { useStorage } from "@ionic/react-hooks/storage";
import { FilesystemDirectory, Capacitor } from "@capacitor/core";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
export const LOG_PREFIX = "[Photos] ";
const Photos: React.FC = () => {
const [photoItems, setPhotoItems] = useState<JSX.Element[] | null>(null);
const { getUri } = useFilesystem();
const { get } = useStorage();
useIonViewWillEnter(async () => {
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
console.log(LOG_PREFIX + "images", images);
if (images && images.length > 0) {
const photos: JSX.Element[] = [];
for await (let image of images) {
console.log(LOG_PREFIX + "checking if file exists", image.fileName);
const finalPhotoUri = await getUri({
directory: FilesystemDirectory.Documents,
path: image.fileName
});
console.log(LOG_PREFIX + "image.fileName", image.fileName);
console.log(LOG_PREFIX + "finalPhotoUri", finalPhotoUri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log(LOG_PREFIX + "converted photoUrl", photoUrl);
photos.push(
<IonItemSliding key={image.fileName}>
<IonItem>
<IonAvatar slot="start">
{photoUrl && <img src={photoUrl} alt="my pic" />}
</IonAvatar>
<IonLabel>{image.fileName}</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption onClick={() => {}}>Delete</IonItemOption>
</IonItemOptions>
</IonItemSliding>
);
}
console.log(LOG_PREFIX + "setPhotoItems");
setPhotoItems(photos);
}
});
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonList>{photoItems}</IonList>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Photos;
view raw Photos hosted with ❤ by GitHub

As you can see we have a new hook called useIonViewWillEnter. This lifecycle hook is one of four event hooks that occur on all IonPage components. The others are useIonViewWillLeave, useIonViewDidEnter, and useIonViewDidLeave. Since we want our file data to arrive before the screen completes drawing, we are placing code to retrieve that data into the useIonViewWillEnter handler. Note the React class lifecycle events are still available, like componentDidMount and the others, but you should avoid them because according to Ionic documentation Ionic has taken over lifecycle control and so these events may not occur at the time you expect them to. Let’s continue. Within the useIonViewWillEnter hook we are using the storage and filesystem hooks to retrieve the file names we had saved earlier and get the photo’s path. In case you’ve not used it before, for await is a newer way of doing a for loop using async await. If there are async calls inside the for loop it will wait on that line before continuing. I added this call to make sure that by the time setPhotoItems is called the photos is fully populated with elements. Now once getUri gives us back the photo path we create some list items that we will later feed to a parent IonList component. These IonItems contain an IonAvatar for our image and an IonLabel for our image name. In addition since this is a native mobile app I thought it would look nicer if we could follow some mobile UI paradigms and so I am also using the IonItemSliding component. This component will allow the listed items to slide out to the left and reveal a Delete button. Let’s start the app again with this latest update.

ionic cap run ios

This is what you should see on your device when you slide an item out.

As you can see the IonItemSliding component also accepts an IonItemOptions component which takes the IonItemOption component that will ultimately handle the Delete button click. But before we add the handler for deletion, let’s add a click event handler to show a larger modal view of each image avatar that is clicked. Here’s the updated Photos.tsx file with the modal code and event handlers.

import React, { useState } from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonLabel,
useIonViewWillEnter,
IonItemSliding,
IonItem,
IonItemOptions,
IonItemOption,
IonAvatar,
IonModal,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonButton
} from "@ionic/react";
import { useStorage } from "@ionic/react-hooks/storage";
import { FilesystemDirectory, Capacitor } from "@capacitor/core";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
import "./Photos.css";
export const LOG_PREFIX = "[Photos] ";
const Photos: React.FC = () => {
const [currentPhotoUrl, setCurrentPhotoUrl] = useState("");
const [currentFileName, setCurrentFileName] = useState("");
const [showModal, setShowModal] = useState(false);
const [photoItems, setPhotoItems] = useState<JSX.Element[] | null>(null);
const { getUri } = useFilesystem();
const { get } = useStorage();
useIonViewWillEnter(async () => {
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
console.log(LOG_PREFIX + "images", images);
if (images && images.length > 0) {
const photos: JSX.Element[] = [];
for await (let image of images) {
console.log(LOG_PREFIX + "checking if file exists", image.fileName);
const finalPhotoUri = await getUri({
directory: FilesystemDirectory.Documents,
path: image.fileName
});
console.log(LOG_PREFIX + "image.fileName", image.fileName);
console.log(LOG_PREFIX + "finalPhotoUri", finalPhotoUri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log(LOG_PREFIX + "converted photoUrl", photoUrl);
photos.push(
<IonItemSliding key={image.fileName}>
<IonItem
data-name={image.fileName}
data-path={photoUrl}
onClick={onClickSelectPhoto}
>
<IonAvatar slot="start">
{photoUrl && <img src={photoUrl} alt="my pic" />}
</IonAvatar>
<IonLabel>{image.fileName}</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption onClick={() => {}}>Delete</IonItemOption>
</IonItemOptions>
</IonItemSliding>
);
}
console.log(LOG_PREFIX + "setPhotoItems");
setPhotoItems(photos);
}
});
const onClickSelectPhoto = (
e: React.MouseEvent<HTMLIonItemElement, MouseEvent>
) => {
const fileName = e.currentTarget.getAttribute("data-name");
const photoUrl = e.currentTarget.getAttribute("data-path");
setCurrentFileName(fileName || "");
setCurrentPhotoUrl(photoUrl || "");
toggleModalPic();
};
const toggleModalPic = () => {
setShowModal(!showModal);
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonList>{photoItems}</IonList>
</IonCol>
</IonRow>
</IonGrid>
<IonModal id="img-modal" isOpen={showModal}>
<IonItem className="ion-align-self-end">
<IonButton onClick={toggleModalPic}>close</IonButton>
</IonItem>
<IonCard>
<IonCardHeader>
<IonCardTitle>{currentFileName}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<img src={currentPhotoUrl} alt="current img" />
</IonCardContent>
</IonCard>
</IonModal>
</IonContent>
</IonPage>
);
};
export default Photos;

Near the top of the Photos functional component you’ll see we have several new state objects currentPhotoUrl, currentFileName, and showModal. These will be used by the IonModal component to display the appropriate information or to toggle the modal window. Now within the useIonViewWillEnter hook we’ve updated the IonItem component to include the attributes data-name and data-path and the onClick event handler. If you look at the onClickSelectPhoto handler you see that we are using those data attributes to set the state objects we created earlier and opening the modal. If you look all the way to the bottom of the Photos file you’ll see the IonModal and related elements. Notice I am using one of the CSS utility classes for alignment and I also have a stylesheet, Photos.css imported, which sets the — height CSS variable of the modal.

#img-modal {--height: 30em;}

Once the modal is opened it uses the properties of the IonItem to display the appropriate file name and photo. Here’s a look at the open modal.

Great now we’re almost done. Let’s create the delete button handler so that users can delete unwanted images. In your Photos.tsx file find the IonItemOption onClick event and replace the dummy function with the name onClickDelete. Let’s also give this component the data-name attribute and pass it the image.fileName. Now In order to create the onClickDelete function we’ll also need to refactor the code inside of useIonViewWillEnter as we need to use that code also in our onClickDelete function. Let me show you the completed code and I’ll go through it.

import React, { useState } from "react";
import {
IonContent,
IonGrid,
IonRow,
IonCol,
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonList,
IonLabel,
useIonViewWillEnter,
IonItemSliding,
IonItem,
IonItemOptions,
IonItemOption,
IonAvatar,
IonModal,
IonCard,
IonCardHeader,
IonCardTitle,
IonCardContent,
IonButton
} from "@ionic/react";
import { useStorage } from "@ionic/react-hooks/storage";
import { FilesystemDirectory, Capacitor } from "@capacitor/core";
import { useFilesystem } from "@ionic/react-hooks/filesystem";
import "./Photos.css";
export const LOG_PREFIX = "[Photos] ";
const Photos: React.FC = () => {
const [currentPhotoUrl, setCurrentPhotoUrl] = useState("");
const [currentFileName, setCurrentFileName] = useState("");
const [showModal, setShowModal] = useState(false);
const [photoItems, setPhotoItems] = useState<JSX.Element[] | null>(null);
const { getUri } = useFilesystem();
const { get, set } = useStorage();
useIonViewWillEnter(async () => {
setIonListItems();
});
const setIonListItems = async () => {
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
console.log(LOG_PREFIX + "images", images);
if (images && images.length > 0) {
const photos: JSX.Element[] = [];
for await (let image of images) {
console.log(LOG_PREFIX + "checking if file exists", image.fileName);
const finalPhotoUri = await getUri({
directory: FilesystemDirectory.Documents,
path: image.fileName
});
console.log(LOG_PREFIX + "image.fileName", image.fileName);
console.log(LOG_PREFIX + "finalPhotoUri", finalPhotoUri);
const photoUrl = Capacitor.convertFileSrc(finalPhotoUri.uri);
console.log(LOG_PREFIX + "converted photoUrl", photoUrl);
photos.push(
<IonItemSliding key={image.fileName}>
<IonItem
data-name={image.fileName}
data-path={photoUrl}
onClick={onClickSelectPhoto}
>
<IonAvatar slot="start">
{photoUrl && <img src={photoUrl} alt="my pic" />}
</IonAvatar>
<IonLabel>{image.fileName}</IonLabel>
</IonItem>
<IonItemOptions side="end">
<IonItemOption onClick={onClickDelete} data-name={image.fileName}>
Delete
</IonItemOption>
</IonItemOptions>
</IonItemSliding>
);
}
console.log(LOG_PREFIX + "setPhotoItems");
setPhotoItems(photos);
}
};
const onClickDelete = async (
e: React.MouseEvent<HTMLIonItemOptionElement, MouseEvent>
) => {
const fileName = e.currentTarget.getAttribute("data-name");
const imgPaths = await get("imagePaths");
const images = imgPaths ? JSON.parse(imgPaths) : null;
const newImgPaths = [];
if (imgPaths) {
for (let i = 0; i < images.length; i++) {
const img = images[i];
if (img.fileName !== fileName) {
newImgPaths.push({
fileName: img.fileName
});
}
}
}
const newImagePaths = JSON.stringify(newImgPaths);
await set("imagePaths", newImagePaths);
setIonListItems();
};
const onClickSelectPhoto = (
e: React.MouseEvent<HTMLIonItemElement, MouseEvent>
) => {
const fileName = e.currentTarget.getAttribute("data-name");
const photoUrl = e.currentTarget.getAttribute("data-path");
setCurrentFileName(fileName || "");
setCurrentPhotoUrl(photoUrl || "");
toggleModalPic();
};
const toggleModalPic = () => {
setShowModal(!showModal);
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Photos</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent>
<IonGrid>
<IonRow>
<IonCol size-md="6" offset-md="3" className="ion-text-center">
<IonList>{photoItems}</IonList>
</IonCol>
</IonRow>
</IonGrid>
<IonModal id="img-modal" isOpen={showModal}>
<IonItem className="ion-align-self-end">
<IonButton onClick={toggleModalPic}>close</IonButton>
</IonItem>
<IonCard>
<IonCardHeader>
<IonCardTitle>{currentFileName}</IonCardTitle>
</IonCardHeader>
<IonCardContent>
<img src={currentPhotoUrl} alt="current img" />
</IonCardContent>
</IonCard>
</IonModal>
</IonContent>
</IonPage>
);
};
export default Photos;

So as shown previously we are using local storage on the device in order to store our file names. The code that was retrieving our file names was initially in the useIonViewWillEnter hook. We have now moved it into a new function called setIonListItems. Now inside of onClickDelete we add some code that retrieves the list of file names, but then creates a new list that does not include the selected file to delete. This new file name list is then once again saved into local storage and then again our setIonListItems function is called which resets the IonItem list of files. Try it out yourself on your device.


That’s it a small but functional photo app that works across iOS, Android, and the Web! I’ve had good results with Ionic so I’m going to update my own application, DzHaven, to use Ionic so I can put it in the app store. I might write about it here as well. Here’s the updated code for this series, https://github.com/dharric/MyPhotos. I hope you try Ionic on your next project. Good luck.

Published by David Choi

Developer Lead DzHaven.com. Where devs help devs and make money.

3 thoughts on “Build Mobile iOS, Android, and Web Apps Using React and Ionic, Part IV

  1. David, this is awesome thanks! I am able to use the camera functionality in web app side with local macbook camera to take a pic, but it is not showing up on the photos tab. Is this because I am not able to access the local storage for the web app version? Or did I mess something up?

    Liked by 1 person

    1. Hi tanner did you install the pwa elements? I would assume you did if you can take a pic from a web browser.
      When you call your getUri what is in the finalPhotoUri? You need to convert finalPhotoUri.uri, using Camera.convertFileSrc, so that the path becomes a url.
      Are you able to set the src attribute of your img element, inside the IonAvatar element, to the url of the photo? The variable name of the url is photoUrl.

      Like

Leave a reply to David Choi Cancel reply

Design a site like this with WordPress.com
Get started