Skip to content
View lessons Login

Launch sale! Get 30% OFF expires in 126:46:22

3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Taking Photos with the Camera API

Taking Photos with the Camera API

For the last application we built, we first considered which feature to build first. Usually, I like to focus on the key functionality of the application first if possible. In this case, we are building an application centered around taking photos, so let’s implement that first.

Create a Photo Interface

export interface PhotoData {
name: string;
path: string;
dateTaken: string;
}

For our Photo entity we are interested in keep tracking of a name which will be used as a file name for that photo, the path to where it is stored on the device, and the date it was take which we will display to the user.

Create a Photo Service

import { Injectable, signal } from '@angular/core';
import { PhotoData } from 'src/app/shared/interfaces/photo';
@Injectable({
providedIn: 'root',
})
export class PhotoService {
photos = signal<PhotoData[]>([]);
}

Pretty simple for now, we just have a photos signal to hold our state.

Create a source for adding photos

Now we are going to add an add$ source for adding new photos to our state. In many ways we are going to be doing more or less the same general thing we did in the Quicklists application: we will have a source, we create a reducer for that source, and that reducer will update the state in some way.

This application will be a little more complex though, as when we trigger our add$ source we will need to trigger asynchronous operators, rather than just having the data we need to add available synchronously immediately.

export class PhotoService {
// sources
add$ = new Subject<void>();
// state
photos = signal<PhotoData[]>([]);
constructor() {
this.add$
.pipe(takeUntilDestroyed())
.subscribe(() => this.photos.update((photos) => [...photos]));
}
}

We can see the same basic redux plumbing here: source > reducer > state. The problem here though is that our add$ source has a type of void. It doesn’t receive any data, it just gets triggered. All our reducer is doing is responding to this nothingness and updating our photos with no new data. So, at the moment, what we are doing is pointless.

What we need to do now is trigger Capacitor’s Camera API when the add$ source is triggered. This is a Promise based API that will return us some photo data when the Promise resolves. We will then need to take that data and update our state with it.

First, we are going to focus on setting up the configuration we need for the Camera API. But take some time to think about how we might end up changing our add$ reducer in the constructor to integrate this asynchronous request to the Camera API (hint: we will be doing it entirely with observables/operators within the pipe of the add$ reducer).

Setting up for the Camera API

npm install @capacitor/camera

Let’s get all the pieces in place first and then we can talk through how it works.

import { inject, Injectable, signal } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
Camera,
CameraResultType,
CameraSource,
ImageOptions,
} from '@capacitor/camera';
import { Platform } from '@ionic/angular';
import { EMPTY, Subject } from 'rxjs';
import { switchMap, catchError } from 'rxjs/operators';
import { PhotoData } from 'src/app/shared/interfaces/photo';
@Injectable({
providedIn: 'root',
})
export class PhotoService {
platform = inject(Platform);
options: ImageOptions = {
quality: 50,
width: 600,
allowEditing: false,
resultType: this.platform.is('capacitor')
? CameraResultType.Uri
: CameraResultType.DataUrl,
source: CameraSource.Camera,
};
// sources
add$ = new Subject<void>();
// state
photos = signal<PhotoData[]>([]);
constructor() {
// reducers
this.add$
.pipe(
switchMap(() => Camera.getPhoto(this.options)),
catchError(() => EMPTY),
takeUntilDestroyed(),
)
.subscribe((photo) =>
this.photos.update((photos) => [
...photos,
{
name: Date.now().toString(),
path: photo.path ?? photo.dataUrl,
dateTaken: new Date().toISOString(),
} as PhotoData,
]),
);
}
}

We will talk about Camera itself in just a moment, but let’s first just notice the general structure here. For our reducer we still just have a direct call to update the photos signal in the subscribe but now we are supplying it with actual data due to the changes we made to the pipe.

We use the switchMap operator to call Camera.getPhoto every time our add$ source is triggered. The getPhoto call will return us a Promise which is automatically converted into an observable for us by switchMap. The data from getPhoto will then be passed to our subscribe when it resolves.

We have also added a catchError in case the getPhoto errors. This is just basic error handling that won’t actually do anything, but it will prevent our stream from breaking entirely by just switching to an EMPTY stream if getPhoto errors. This means that the next time add$ is triggered it will still work, even if one of the previous calls erroed catchError in case the getPhoto errors. This is just basic error handling that won’t actually do anything, but it will prevent our stream from breaking entirely by just switching to an EMPTY stream if getPhoto errors. This means that the next time add$ is triggered it will still work, even if one of the previous calls errored.

The call to the Camera plugin itself is reasonably straightforward:

Camera.getPhoto(this.options);

But it’s the options for taking the photo that are a little more interesting. Some of these options are just general configurations like the quality of the image, whether editing should be allowed, whether to take a new photo with the Camera or to use the existing Photos in the users gallery, the size, and there are other options available too.

But then we have this:

STANDARD STANDARD

Thanks for checking out the preview of this lesson!

You do not have the appropriate membership to view the full lesson. If you would like full access to this module you can view membership options (or log in if you are already have an appropriate membership).