Swift and Capacitor

Running Custom Native iOS Code in Ionic with Capacitor



·

One of the limiting factors of the hybrid application approach is the reliance on a mostly community driven plugin ecosystem to access native functionality with Cordova. Most of the time you can easily find a Cordova plugin that provides the functionality you want. However, sometimes for more niche requirements you may struggle to find an existing plugin that does what you need, or perhaps you can find one but it is no longer maintained.

If you don’t know how to take matters into your own hands and implement the native functionality yourself, you will hit a brick wall and you will either need to change the requirements of your application or give up.

Capacitor is aiming to make the process of integrating native code into your Ionic projects (or whatever you are using Capacitor with) a little more approachable. You will still need to write native code for the platform you are targeting (as you could with Cordova), but the process of creating a plugin to expose native functionality to your web-based application is quite straight-forward with Capacitor.

In this tutorial, we will be adding our own native iOS code (Objective-C/Swift) to an Ionic/Capacitor project. We will create a custom local Capacitor plugin that allows us to automatically grab the latest photo in the user’s photo library, and then we will use that in an Ionic application to display the photo:

Get Latest Photo Capacitor Plugin Example

NOTE: I have no idea if there is a plugin that exists already that does this, but let’s pretend there isn’t. The point of this exercise is to see how difficult it is for us to run our own native code when none of the existing plugins satisfy our needs.

Context

A lot of developers who use Ionic do so because they want to build stuff with web tech. If we have to learn Objective-C/Swift/Java to add native code to our applications doesn’t that kind of defeat the purpose? To that I would say:

  1. Not having to write native code is not the only benefit of using a hybrid approach. I would argue the main benefit is being able to create an application with a single codebase that runs anywhere the web does.
  2. You would rarely ever need to write native code because most of the time there will be existing plugins to do what you need. In the case that there isn’t, though, a little bit of native code can remove a roadblock from your project.

I have very little experience with native iOS/Android development. I have worked a little bit with Java and have developed some native Android applications a while ago, but I have next to no experience with Objective-C/Swift. I think it is then interesting to see how difficult it would be for someone like me, whose experience is mostly with web tech, to successfully integrate some custom native functionality with a bit of Googling.

I would like to preface this tutorial by saying that I don’t think it is a good idea to rely on cobbling together solutions you don’t understand from StackOverflow to build your application – this is an almost guaranteed way to build a buggy and unmaintainable application. However, as I mentioned, most of the time you won’t need to do this. If you just need to add 50 lines of Swift or Java that you don’t completely understand to your project to get past a roadblock and back into web land, then I don’t really think it’s a big deal.

NOTE: In order to complete this tutorial, you will need macOS and XCode.

Before We Get Started

This tutorial assumes at least a basic level of knowledge of Ionic. Although you do not need to have a solid understanding of Capacitor, it will be helpful to at least understand what the role of Capacitor is.

Create a new Ionic Application

To get started, we are just going to create a new Ionic application with the following command:

ionic start capacitor-native-ios-plugin blank

If prompted, don’t integrate Cordova with the application (we will be using Capacitor, of course!).

Set up Capacitor

Since Capacitor is currently in alpha, I don’t want to add the exact installation instructions here as they will likely change. Before continuing, make sure to follow the installation steps in the documentation to integrate Capacitor with your project.

You should also make sure that you have installed at least the LTS version of Node (currently version 8.9.4). Capacitor will not work with some older versions of Node. An easy way to manage Node versions on your system is to use this packagen will allow you to easily switch between Node versions.

Since we are working with iOS, you should make sure that you have all of the required dependencies (make sure to install CocoaPods).

Finally, make sure you add iOS to your Capacitor project with:

npx cap add ios

What Does a Plugin Look Like?

Before we build our own local plugin, I think it is useful to look at the existing Capacitor plugins to get the general idea of how they work. You can view all of the default Capacitor plugins by going to the following folder:

ios > App > Pods > Capacitor > ios > Capacitor > Capacitor > Plugins

Capacitor Plugin File Structure

You will find that each of the plugins has a .swift file that defines the plugins functionality, and then there is a DefaultPlugins.m file that registers all of the default plugins. Open the Camera.swift file and take a look around.

Confused? Unless you actually do have some Swift experience I would be surprised if you weren’t. The Swift syntax is quite similar to regular JavaScript, so it isn’t completely foreign, but there are a lot of strange things going on. Fortunately, you don’t need to understand most of it – we just need to know enough to get our plugin working.

The important parts in this file are the class declaration:

@objc(CAPCameraPlugin)
public class CAPCameraPlugin : CAPPlugin, UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {

The @objc decorator is important, as it is what makes this plugin visible to Capacitor. The only other really important part of this file to understand is the function that is also prefixed with an @objc decorator:

@objc func getPhoto(_ call: CAPPluginCall) {

}

This is the function that will be exposed to our Ionic application in the web view. You can have other functions defined inside of the class that don’t use the @objc decorator, but they will only be used internally and not exposed to your application for use. This function has a call parameter passed into it. This is what we use to communicate the results back to our Ionic application. You can either use call.error:

call.error("User denied access to photos")

to indicate that an error occurred. Or, you can use call.success to pass back the required data:

call.success([
    "someData": "hello!"
])

The basic idea is that you:

  1. Define a function that will be callable from your web view
  2. Run whatever native code is required
  3. Pass the information back to the web view using call

A very basic plugin might look like this:

import Capacitor
import SomeLibrary

@objc(MyCoolPlugin)
public class MyCoolPlugin: CAPPlugin {

    @objc func getLastPhotoTaken(_ call: CAPPluginCall) {

        // do something native

        call.success([
            "someResult": "hello!"
        ])

    }

}

It is worth noting that if you just need to run some native code and there is no need to expose the result to your application running in the web view, then there is no need to create a plugin. You can just add the native code directly to your project.

Enjoying learning how to build Capacitor plugins? Consider retweeting to share with others 😀

Create the Plugin

Now that we have a basic idea of how a Capacitor plugin works, let’s add our own. All we are going to do is add a new plugin directly to our local project. The Capacitor CLI actually provides a way to easily generate standalone plugins that you can publish to npm and install just like any normal plugin. However, this tutorial is just going to focus on manually adding code to the local project, and I will likely cover creating the plugin “properly” in another tutorial.

Since Capacitor is so new, I’m not sure that where I have placed these files, or the way I have designed them, is necessarily “best practice” for Capacitor projects – if anybody in the know is around, feel free to chime in!

In order to add our plugin, we will be adding a new .swift file to the same folder as the rest of the Capacitor plugins. We will also need to add a new .m file to register the plugin. Before we can start building that plugin, we need to figure out how we can get the latest photo from the user’s library using Swift.

Again, I have no idea how to actually do this. After a bit of Googling and looking at the existing Capacitor plugins, I was able to piece together this code:

    func getLastPhotoTaken() {

        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        fetchOptions.fetchLimit = 1

        let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
        let image = getAssetThumbnail(asset: fetchResult.object(at: 0))
        let imageData:Data =  UIImagePNGRepresentation(image)!
        let base64String = imageData.base64EncodedString()

        // base64String should now contain the photo

    }

    func getAssetThumbnail(asset: PHAsset) -> UIImage {

        let manager = PHImageManager.default()
        let option = PHImageRequestOptions()

        var thumbnail = UIImage()
        option.isSynchronous = true

        manager.requestImage(for: asset, targetSize: CGSize(width: 500, height: 500), contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in
            thumbnail = result!
        })

        return thumbnail
    }

The end result of these functions is that it produces a base64 string that contains the image data of the latest photo in the user’s photo library. Now that we have the code to do the job, we just need to work that into a Capacitor plugin.

Create a new file at ios/App/Pods/Capacitor/ios/ Capacitor/Capacitor/Plugins/ PluginTest.swift and add the following:

import Capacitor
import Photos

@objc(PluginTest)
public class PluginTest: CAPPlugin {

    @objc func getLastPhotoTaken(_ call: CAPPluginCall) {

        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        fetchOptions.fetchLimit = 1

        let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
        let image = getAssetThumbnail(asset: fetchResult.object(at: 0))
        let imageData:Data =  UIImagePNGRepresentation(image)!
        let base64String = imageData.base64EncodedString()

        call.success([
            "image": base64String
        ])

    }

    func getAssetThumbnail(asset: PHAsset) -> UIImage {

        let manager = PHImageManager.default()
        let option = PHImageRequestOptions()

        var thumbnail = UIImage()
        option.isSynchronous = true

        manager.requestImage(for: asset, targetSize: CGSize(width: 500, height: 500), contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in
            thumbnail = result!
        })

        return thumbnail
    }

}

All we’ve done here is add the parts necessary for Capacitor to recognise this as a plugin, and expose the function/result to our web view. Eventually, we will be able to access this function through our application using PluginTest.getLastPhotoTaken() and it will return us the base64 encoded image.

Now we just need to create the .m file to register the plugin. The documentation notes that you should create this file using the New File... interface in XCode. Make sure to do that.

Add the following code to ios/App/Pods/Capacitor/ios/ Capacitor/Capacitor/Plugins/ PluginTest.m

#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>

CAP_PLUGIN(PluginTest, "PluginTest",
           CAP_PLUGIN_METHOD(getLastPhotoTaken, CAPPluginReturnPromise);
)

Use the Plugin

Our plugin should be all ready to go now! Now we just need to add some code to our Ionic application to make use of it.

Modify src/pages/home/home.ts to reflect the following:

import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';
import { Plugins } from '@capacitor/core';
import { DomSanitizer } from '@angular/platform-browser';

declare global  {
    interface PluginRegistry {
        PluginTest?: PluginTest;
    }
}

interface PluginTest {
    getLastPhotoTaken(): Promise<any>;
}

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

    private lastPhoto: string = 'http://placehold.it/500x500';

    constructor(public navCtrl: NavController, private domSanitizer: DomSanitizer) {

    }

    getLatestPhoto(){

        const { PluginTest } = Plugins;

        PluginTest.getLastPhotoTaken().then((result) => {
            this.lastPhoto = "data:image/png;base64, " + result.image;
        });

    }

}

To avoid TypeScript errors, it is important to extend the PluginRegistry interface with your new plugin. For convenience, I have just added that directly to the home page, but I would be interested in hearing where a more appropriate place for these definitions would be.

We have created a getLatestPhoto function that we will call from our template, and inside of that we just use our new PluginTest plugin like we would any other Capacitor plugin. We assign the result of this to this.lastPhoto which we will use to display the image in our template (which is why we have included the DomSanitizer – by default you can’t display base64 images like this).

Modify src/pages/home/home.html to reflect the following:

<ion-header>
  <ion-navbar color="primary">
    <ion-title>
      Custom iOS Native
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>

    <button full ion-button (click)="getLatestPhoto()">Get Latest Photo</button>

    <img [src]="domSanitizer.bypassSecurityTrustUrl(lastPhoto)" />

</ion-content>

Modify src/pages/home/home.scss to reflect the following:

page-home {

    img {
        display: block;
        width: 100%;
        height: auto;
    }

}

NOTE: There is a slight delay in fetching and returning the photo. For the sake of UI/UX we should displace some kind of placeholder or loading indicator whilst the photo is being fetched, but that’s a bit out of scope for this tutorial.

Run the Application

We’re all done! Now we just need to run the application. Before attempting to run the application, make sure that you build it:

npm run build

and copy the web directory to your native project with:

npx cap copy

you can then run:

npx cap open ios

and run the application using XCode. You should then be able to tap the “Get Latest Photo” button and see the last photo that was taken on your device – if it’s not too embarrassing, feel free to share it in the comments!

Summary

This was quite a fun exercise, and it adds a lot of confidence knowing that, if you need to, you have the tools available to run your own custom native code. The more comfortable you are with Swift or Java the better, but I don’t think it is unreasonable to develop basic plugins like this even if you have little to no knowledge of the native code.

UPDATE: A tutorial on building this plugin as an installable npm package is available now.

What to watch next...