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
Create a file at
src/app/shared/interfaces/photo.ts
and add the following:
export interface Photo {
name: string;
path: string;
dateTaken: string;
}
For our Photo
entity we are interesting 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
Create a file at
src/app/home/data-access/photo.service.ts
and add the following:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { Photo } from '../../shared/interfaces/photo';
@Injectable({
providedIn: 'root',
})
export class PhotoService {
#photos$ = new BehaviorSubject<Photo[]>([]);
photos$ = this.#photos$.asObservable();
}
This is once again the same general concept we have been using for keep track of data in services. We create a BehaviorSubject
which we will use to emit new values on.
However, we are taking a slightly different approach to how we expose that stream to the rest of our application. Rather than having something like a getPhotos
method that will return the stream as an observable (not a subject) we instead use these private and public class members:
#photos$ = new BehaviorSubject<Photo[]>([]);
photos$ = this.#photos$.asObservable();
The #
syntax we use with #photos$
is actually a special type of syntax to create Private fields - it is worth spending a brief moment explaining the difference between the private
keyword and a private field denoted with the #
syntax.
The private
keyword will make whatever class member we add it to only accessible from within the class it was defined with - at compile time. This means the TypeScript compiler will complain if you try to access a private
class member from outside of the class.
However, a private field created with #
will prevent accessing the value both at compile time and during runtime. So, technically, using #
is a bit “stronger” than private
…
…that isn’t why we are using it here though. We basically want two streams:
- A private
BehaviorSubject
that is only to be used by the service - A public
Observable
of thatBehaviorSubject
that can be consumed by the rest of the application.
We can’t do this:
private photos$ = new BehaviorSubject<Photo[]>([]);
public photos$ = this.photos$.asObservable();
Because we can’t declare photos$
twice. So, we would have to do something like rename one of them:
private photosSubject$ = new BehaviorSubject<Photo[]>([]);
public photos$ = this.photosSubject$.asObservable();
This is fine, but if you are using the default ESLint rules it is going to complain about private
members being declared before public
members. We need to do this because we need our photosSubject$
to exist before we try to use it, so we would have to manually ignore that ESLint rule. This is also fine, just a bit annoying.
I think this:
#photos$ = new BehaviorSubject<Photo[]>([]);
photos$ = this.#photos$.asObservable();
Is just a nice and easy way to achieve a private/public version of the stream.
Create the addPhoto Method
Add the following method to the
PhotoService
:
private addPhoto(fileName: string, filePath: string) {
const newPhotos = [
{
name: fileName,
path: filePath,
dateTaken: new Date().toISOString(),
},
...this.#photos$.value,
];
this.#photos$.next(newPhotos);
}
This is still utilising the same concepts we have already covered. The role of this method is to receive a fileName
and filePath
, create a new photo from that using the current date, add it into the existing photos array, and emit it on the stream. Notably in this example we are adding the photo to the beginning of the array as we want our latest photos to display at the top of the list.
Create the takePhoto Method
Now we are going to implement the ability to actually take a photo using the camera. To do this, we are going to need to install the Camera plugin for Capacitor.
Install the following package:
npm install @capacitor/camera
NOTE: In order for the the Camera to work on iOS and Android you will also need to follow these instructions to add usage descriptions to the native iOS and Android platforms (i.e. you will need to run ionic cap open ios
and ionic cap open android
and manually make these configuration changes in Xcode/Android Studio. If you don’t intend to run this application natively right now, then you don’t need to worry about this).
Inject
Platform
from@ionic/angular
asplatform
through theconstructor
of thePhotoService
Add the following method to the
PhotoService
:
async takePhoto() {
const options: ImageOptions = {
quality: 50,
width: 600,
allowEditing: false,
resultType: this.platform.is('capacitor')
? CameraResultType.Uri
: CameraResultType.DataUrl,
source: CameraSource.Camera,
};
try {
const photo = await Camera.getPhoto(options);
if (photo.path) {
this.addPhoto(Date.now().toString(), photo.path);
} else if (photo.dataUrl) {
this.addPhoto(Date.now().toString(), photo.dataUrl);
}
} catch (err) {
console.log(err);
throw new Error('Could not save photo');
}
}
NOTE: You will require the following imports from the Camera plugin:
import {
Camera,
CameraResultType,
CameraSource,
ImageOptions,
} from '@capacitor/camera';