3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 4

Taking Photos with the Camera API

Integrating native functionality

STANDARD

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 that BehaviorSubject 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 as platform through the constructor of the PhotoService

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';
STANDARD
Key

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).