Developing cross platform apps with Ionic Capacitor - part 2

March 26, 2018, 7:00 am Categories:

Categories

In part 1 of this tutorial we began our exploration of using the recently released Ionic Capacitor software; familiarising ourselves with its system requirements as well as an awareness of iOS & Android development caveats before creating the initial foundations for an Ionic project that we'll develop in this second tutorial of a three-part series.

This development will involve making use of the built-in Capacitor Camera plugin to eventually build an application that runs on both iOS/Android platforms as well as the Web.

Recap: what we'll be creating

Our application, imaginatively titled ionic-camera, consists of a single screen which allows the user to select an image (which will be subsequently rendered to the screen) based on the following contexts:

  • iOS/Android - Capacitor's Camera plugin will be utilised to capture image from the device camera or retrieve those courtesy of the device Photolibrary instead (both options will be presented and made available to the user)
  • Web - A simple File input button along with use of the HTML5 Filereader API will serve to allow images to be selected from the user's system and returned for display on the screen

The ionic-camera application will appear as follows (demonstrated in use on an iOS device):

iOS device running an Ionic Capacitor project demonstrating a single page application with device camera functionality enabled

As you can see it's a very basic UI (the splash screen image I have designed and added to the ionic-camera/ios/App/App/Assets/xcassets/Splash.imageset directory - we'll discuss this in the third and final part of this tutorial series) but the focus here isn't on UI design but rather a demonstration of how Capacitor can be used to create cross platform mobile apps capable of running on Web as equally as mobile.

Notice, in the last screen capture of the above graphic, how the user can choose to capture an image using the device camera or select one from the device Photolibrary (courtesy of the Ionic ActionSheet Component)?

Giving the user the choice of how to retrieve their desired image (and make use of available device features) is always a nice UX feature when developing for mobile.

The following screen captures demonstrate the selection of an image from the device Photolibrary and its subsequent display on the screen:

Screen captures demonstrating the device Photolibrary on iOS being accessed and the user selecting an image which is then displayed on the screen

Given, from the foundations we laid in the first tutorial, that we have the bare bones of the application already in place let's now focus on developing this through to its conclusion.

Splash screen considerations

Capacitor provides its own Splash Screen API which, as you probably already guessed, can be used to control the timing for the display/non-display of the application's splash screen.

To use this within our applications we begin by importing @capacitor/core and declaring the Splash Screen Plugin API like so:

import { Plugins } from '@capacitor/core';

const { SplashScreen } = Plugins;

This would then be called, typically within the application's root component, in the following way:

constructor(platform     : Platform, 
            statusBar    : StatusBar) 
{
   SplashScreen.hide();
}

You might remember, from the previous tutorial, that we uninstalled the Ionic Native Splash Screen plugin and, as a consequence of this, we also remove ALL references to that from the root component.

This is necessary as we're making use of Capacitor NOT Ionic Native to provide (and handle) this particular functionality.

In full then the Splash Screen API is implemented within the ionic-camera/src/app/app.component.ts root component like so:

import { Component } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { Plugins } from '@capacitor/core';

const { SplashScreen } = Plugins;


import { HomePage } from '../pages/home/home';
@Component({
  templateUrl: 'app.html'
})
export class MyApp {


   rootPage:any = HomePage;




   constructor(platform     : Platform, 
               statusBar    : StatusBar) 
   {
      SplashScreen.hide();
      
      platform
      .ready()
      .then(() => 
      {
         if(platform.is('ios') || platform.is('android'))
         {
            SplashScreen.hide();
         }
      });
   }
}

Unfortunately (and I don't know whether this is a mistake/misunderstanding on my part or whether it's due to Capacitor still being in alpha release - at the time of writing at least) even with the Splash Screen plugin implemented I nonetheless receive warnings in the Xcode console informing me that the Splash Screen is taking too long to execute.

I've tried running this within and outside of the platform.ready() method inside the root component constructor but to no avail.

Why this is I'm not sure but if any developers out there have an answer I'm all ears!

Handling image selection and parsing

As the purpose of the ionic-camera application is to select and display photographic images it makes sense that we handle such functionality within its own service (regular readers of this blog will know only too well that I'm a huge advocate of using Angular services to handle dedicated/repetitive functionality).

To this end we'll make use of the Capacitor Camera plugin API within the ImageProvider service we created in the first part of this tutorial.

Open the ionic-camera/src/providers/image/image.ts service and add the following code (documented in full to explain what's happening at each stage of the script; particularly where certain properties/methods & techniques are being used):

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
import { Plugins, CameraResultType, CameraSource } from '@capacitor/core';
import { Observable } from 'rxjs/Observable';


const { Camera } 		= Plugins;


@Injectable()
export class ImageProvider {



   /**
    * @name _READER
    * @type object
    * @private
    * @description              Creates a FileReader API object
    */
   private _READER 					: any  			=	new FileReader();




   /**
    * @name _IMAGE
    * @type object
    * @private
    * @description              Create an image object using the Angular SafeResourceUrl 
    * 							Interface property to define a URL as safe for loading 
    *							executable code from
    */
   private _IMAGE 					: SafeResourceUrl;




   constructor(public http 			: HttpClient, 
   			   private sanitizer 	: DomSanitizer) 
   {  }




   /* ----------------------------------------------------------------

      Mobile environment specific methods - used for iOS/Android only

      ---------------------------------------------------------------- */




   /**
    * @public
    * @method takePicture
    * @description    			Uses the getPhoto method of the Capacitor Camera plugin 
    *							API to return a file Uri which is then made available 
    *							to the parent script as a resolved (or rejected) Promise 
    * 							object courtesy of the async/await functions
    * 							
    * @return {Promise}
    */
   async takePicture() : Promise
   {

      /* Define the options for the getPhoto method - particularly the source for where 
         the image will be taken from (I.e. the device camera) and how we want the captured 
         image data returned (I.e. base64 string or a file uri) */
      const image  	= await Camera.getPhoto({
         quality 		: 	90,
         allowEditing 	: 	true,
         resultType 	: 	CameraResultType.Uri,
         source 		: 	CameraSource.Camera
      });



      /* We need to run the returned Image URL through Angular's DomSanitizer to 'trust' 
         this for use within the application (I.e. so that Angular knows this isn't an 
         XSS attempt or similarly malicious code) */
      this._IMAGE 		= this.sanitizer.bypassSecurityTrustResourceUrl(image && (image.webPath));
      return this._IMAGE;
   }




   /**
    * @public
    * @method selectPhoto
    * @description    			Uses the getPhoto method of the Capacitor Camera plugin 
    *							API to return a file Uri from the Photolibrary selected 
    *							image which is then made available to the parent script 
    *							as a resolved (or rejected) Promise object courtesy of the 
    *							async/await functions
    * 							
    * @return {Promise}
    */
   async selectPhoto() : Promise
   {

      /* Define the options for the getPhoto method - particularly how we want the
         image data returned (I.e. base64 string or a file uri) */
      const image 	= await Camera.getPhoto({
         quality 		:	90,
         allowEditing 	: 	false,
         resultType 	: 	CameraResultType.Uri
      });


      // We return the webPath property of the image object (which contains the image path)
      return image.webPath;
   }




   /* ----------------------------------------------------------------

      Web environment specific methods - used for Progressive Web Apps

      ---------------------------------------------------------------- */



   /**
    * @public
    * @method selectImage
    * @param event  {any}     	The DOM event that we are capturing from the File input field
    * @description    			Uses the FileReader API to capture the input field event, 
    *							retrieve the selected image and return that as a base64 data 
    *							URL courtesy of an Observable
    * @return {Observable}
    */
   selectImage(event) : Observable
   {
      return Observable.create((observer) =>
      {
         this.handleImageSelection(event)
         .subscribe((res) =>
         {      
            observer.next(res);
            observer.complete();   
         });
      });
   }




   /**
    * @public
    * @method handleImageSelection
    * @param event  {any}     	The DOM event that we are capturing from the File input field
    * @description    			Uses the FileReader API to capture the input field event, 
    *							retrieve the selected image and return that as a base64 data 
    *							URL courtesy of an Observable
    * @return {Observable}
    */
   handleImageSelection(event : any) : Observable
   {
      let file 		: any 		= event.target.files[0];

      this._READER.readAsDataURL(file);
      return Observable.create((observer) =>
      {
         this._READER.onloadend = () =>
         {
            observer.next(this._READER.result);
            observer.complete();
         }
      });
   }

}

This should be fairly self-explanatory from the use of naming conventions and documentation for each of the above properties and methods but I want to spend a little while lingering on the following aspects of the ImageProvider service:

  • async/await
  • Mobile/Web only methods

Some of you may be unfamiliar with the async/await functions but these are used to handle promises - as the following Mozilla documentation makes clear:

The purpose of async/await functions is to simplify the behavior of using promises\rn synchronously and to perform some behavior on a group of Promises. Just as Promises are\rn similar to structured callbacks, async/await is similar to combining generators and\rn promises.

These then are simply a convenient way to handle promises.

Remember I stated at the beginning of this tutorial that the ionic-camera application would be able to run on both Mobile and Web platforms?

The ImageProvider service handles how images are selected/parsed for these platforms with the following division of methods:

Environment Methods
Mobile takePicture()
selectPhoto()
Web selectImage()
handleImageSelection()

We'll see how this separation is handled within the HomePage component using the Ionic Platform API next.

Developing the HomePage logic

As we're developing a single page application the HomePage component class will contain all of the required logic for handling user interactions with the device Camera/Photolibrary (with help provided by methods from the ImageProvider service) as well as managing the rendering of selected images to the component view.

Along the way we'll be making use of the following Ionic components to improve usability, display messages in addition to providing a much improved user experience:

Open the ionic-camera/src/pages/home/home.ts component class and add the following code (documented in full to explain what's happening at each stage of the script; particularly where certain properties/methods & techniques are being used):

import { Component } from '@angular/core';
import { ActionSheetController, 
         AlertController, 
         NavController, 
         Platform } from 'ionic-angular';
import { ImageProvider } from '../../providers/image/image';


@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {




   /**
    * @name platformIs
    * @type String
    * @public
    * @description               Property that stores the environment reference and is 
    *                            used as a flag for determining which features to 
    *                            'switch on' inside the component template
    */
   public platformIs 				: string 		=	'';




   /**
    * @name imageSource
    * @type String
    * @public
    * @description               Property that stores the retrieved image file from the 
    *                            selected method of the ImageProvider service for 
    *                            rendering inside the component template
    */
   public imageSource 				: string 		=	'';




   constructor(public navCtrl 		: NavController,
               private _ALERT 		: AlertController,
               private _PLAT 		: Platform,
               private _IMAGE 		: ImageProvider,
               private _ACTION 		: ActionSheetController) 
   {

      // Are we on mobile?
      if(this._PLAT.is('ios') || this._PLAT.is('android'))
      {
         this.platformIs = 'mobile';
      }

      // Or web?
      else
      {
         this.platformIs = 'browser';
      }
   }




   /**
    * @public
    * @method selectImage
    * @param event  {any}        The DOM event that we are capturing from the File input field
    * @description               Web only - Returns the image selected by the user and renders 
    *                            this to the component view
    * @return {none}
    */
   selectImage(event : any) : void
   {
      this._IMAGE
      .selectImage(event)
      .subscribe((res) =>
      {
         this.imageSource = res;
      });
   }




   /**
    * @public
    * @method captureImage
    * @description               Mobile only - Launches the ActionSheet component to allow the 
    *                            user to select whether they are to capture an image using the
    *                            device camera or photolibrary
    * @return {none}
    */
   captureImage() : void
   {
      this.launchActionSheet();
   }




   /**
    * @public
    * @method parseImage
    * @param mode  {string}      The device feature that was used (camera or library)
    * @description               Mobile only - Renders the selected image to the component view
    * @return {none}
    */
   parseImage(mode : string) : void
   {
      switch(mode) {

          // Handle image requests via the device camera
      	 case "camera":
            
            this._IMAGE
            .takePicture()  
            .then((data) =>
            {
               this.imageSource = data;
            })
            .catch((error) =>
            {
               console.dir(error);
               this.displayErrorWarning(error);
            });
         break;



         // Handle image requests via the device photolibrary
         case "library":
            
            this._IMAGE
            .selectPhoto()
            .then((data) =>
            {
               this.imageSource = data;
            })
            .catch((error) =>
            {
               console.log('ERROR - Returning the selectPhoto method data in a Promise');
               console.dir(error);
               this.displayErrorWarning(error);
            });
         break;

      }
   }




   /**
    * @public
    * @method launchActionSheet
    * @description               Mobile only - Uses the ActionSheet component to present the 
    *                            user with options to select an image using the device camera 
    *                            or photolibrary
    * @return {none}
    */
   launchActionSheet() : void
   {
      let action  	= this._ACTION.create({
         title 		: 'Select your preferred image source',
         buttons 	: [
            {
               text 	: 'Camera',
               handler 	: () =>
               {
                  this.parseImage('camera');
               }
            },
            {
               text 	: 'Photolibrary',
               handler 	: () =>
               {
                  this.parseImage('library');
               }
            },
            {
               text 	: 'Cancel',
               handler 	: () =>
               {
                  console.log('Cancelled');
               }
            }
         ]
      });
      action.present();
   }




   /**
    * @public
    * @method displayErrorWarning
    * @param message  {string}      A description of the returned error
    * @description      Displays an alert window informing the user of an error
    *                   that has occurred      
    * @return {none}
    */
   displayErrorWarning(message : string) : void
   {
      let alert : any = this._ALERT.create({
         title          : 'Error',
         subTitle       : message,
         buttons      : ['Ok']
            
      });
      alert.present();
   }

}

The code for our HomePage component class should be fairly self explanatory as we are simply:

  • Using the Ionic Platform API to determine whether the application is running on Web or iOS/Android
  • Supplying image capture/parsing methods (that will rely on the ImageProvider service) for web and mobile usage
  • Using the ActionSheet component to display image capture options on mobile
  • Using the AlertController component to display error messages for failed operations

With that stated let's now cover the templating for the HomePage component and see how the image selection for web/mobile environments are catered for in this piece of the application.

Templating

The HTML for the HomePage component is pretty straightforward and simple relies on use of the platformIs property (that we defined within the component class) within an Angular NgIf directive to determine whether a HTML File input is displayed for a web environment or a <button> component displayed instead for an iOS/Android environment.

This is all handled within the ionic-camera/src/pages/home/home.html template courtesy of the following markup structure:

<ion-header>
   <ion-navbar>
      <ion-title>
         Snappa
      </ion-title>
   </ion-navbar>
</ion-header>

<ion-content padding>


    <!-- If we have selected an image display it :) -->
    <img *ngIf="imageSource != ''" [src]="imageSource">


    <div>

       <!--  If we are on a non-iOS/Android environment we display 
             the HTML file input -->
       <div 
          *ngIf="platformIs == 'browser'"
          class="upload-button-wrapper" 
          color="primary">
          <ion-icon name="image"></ion-icon>
          <small>Select image...</small>
          <input
             class="file-upload-button" 
             type="file" 
             (change)="selectImage($event)">
       </div>


       <!--  Otherwise if we are using an iOS/Android environment we 
             display the <button> component instead -->
       <button  
          *ngIf="platformIs == 'mobile'" 
          ion-button 
          block 
          color="primary"
          (click)="captureImage()">
          <ion-icon ios="ios-camera" md="md-camera"></ion-icon>
       </button>

   </div>

</ion-content>

Told you the markup structure was simple didn't I? :)

Now let's add the necessary styles to change the appearance of the template's file input region for selecting images when the application is viewed in a web browser.

To do this simply open the ionic-camera/src/pages/home/home.scss stylesheet and add the following style rules:

page-home {
   
   .upload-button-wrapper {
      position: relative;
      overflow: hidden;
      margin: 10px;
      padding: 1em;
      background: rgba(220, 220, 200, 1);
      border-radius: 10px;
      display: inline-block;


      small {
         font-size: 1em;
         padding: 0.1em 0 0 1em;
         font-family: Verdana, Helvetica, sans-serif;  
      }

      .file-upload-button {
         position: absolute;
         top: 0;
         left: 0;
         margin: 0;
         padding: 1em;
         font-size: 1.2em;
         cursor: pointer;
         opacity: 0;
         filter: alpha(opacity=0); // Play nice with older version of Internet Explorer :)
      }
   }
}

With the application codebase now completed all that remains is to actually build this for deployment to mobile devices and the web.

We'll cover this in the third and final part of our tutorial.

In summary

As a spiritual successor to both Apache Cordova and Adobe PhoneGap Capacitor is surprisingly easy to work with; thanks in large part to its intuitive syntax and the absence of additional plugins/packages that need to be installed (as is the case with Ionic Native).

For a product that is still in alpha release (as of March 2018 - that status may have changed by the time you read this tutorial) it's surprisingly mature, reliable and very quick to pick up and begin working with (it does, of course, help that Capacitor integrates smoothly with Ionic so certain potential headaches in the development process are eliminated by default).

In the final part of this instalment of tutorials on working with Capacitor we'll see how to build our application for deployment to iOS, Android and the Web.

If you've enjoyed what you've read and/or found this helpful please feel free to share your comments, thoughts and suggestions in the comments area below.

I explore different aspects of working with the Ionic framework in my e-book featured below and if you're interested in learning more about further articles and e-books that I'm writing please sign up to my FREE mailing list.

Tags

Categories

Post a comment

All comments are welcome and the rules are simple - be nice and do NOT engage in trolling, spamming, abusiveness or illegal behaviour. If you fail to observe these rules you will be permanently banned from being able to comment.

Top