One of the limiting factors of the “hybrid” application - having the majority of your application and user interface running inside of a web view embedded in a native 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. I don’t think we should view the shortcomings of open source plugins as a negative aspect of Ionic or Cordova or Capacitor. The work we are able to utilise from community plugins is work we would have otherwise had to do ourselves, so when the plugins work that’s great, but if you are relying solely on software maintained by potentially just one person in their free time it is a recipe for disaster.
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.
View the video version of this this tutorial:
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:
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:
- 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.
- 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 straight-up native iOS/Android development. I have worked a little bit with Java and have developed some native Android applications a while ago, but all the experience I have with Objective-C/Swift has been within the context of using Ionic. 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. In the long term, you will begin to understand the native code that you are using more and more.
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 --type=angular
Set up Capacitor
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. Capacitor will not work with some older versions of Node. An easy way to manage Node versions on your system is to use this package – n
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.
Finally, make sure you add iOS to your Capacitor project with:
ionic 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
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.reject
:
call.reject('User denied access to photos');
to indicate that an error occurred. Or, you can use call.resolve
to pass back the required data:
call.resolve([('someData': 'hello!')]);
The basic idea is that you:
- Define a function that will be callable from your web view
- Run whatever native code is required
- 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.resolve([
"someResult": "hello!"
])
}
}
It is worth noting that if you just need to run some native code and there is no need to trigger it from your application or 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.
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.
In order to add our plugin, we will be adding a new .swift
file to the main folder for our iOS application (i.e. the same folder where the AppDelegate.swift
, Info.plist
, and config.xml
files are). We will also need to add a new .m
Objective-C 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 photos = PHPhotoLibrary.authorizationStatus()
if photos == .notDetermined {
PHPhotoLibrary.requestAuthorization({status in
if status == .authorized{
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.fetchLimit = 1
let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let image = self.getAssetThumbnail(asset: fetchResult.object(at: 0))
let imageData:Data = image.pngData()!
let base64String = imageData.base64EncodedString()
// base64String should now contain the photo
} else {
// failed
}
})
}
}
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 App/PluginTest.swift (in the same folder as
AppDelegate.swift
) and add the following:
import Capacitor
import Photos
@objc(PluginTest)
public class PluginTest: CAPPlugin {
@objc func getLastPhotoTaken(_ call: CAPPluginCall) {
let photos = PHPhotoLibrary.authorizationStatus()
if photos == .notDetermined {
PHPhotoLibrary.requestAuthorization({status in
if status == .authorized{
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.fetchLimit = 1
let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
let image = self.getAssetThumbnail(asset: fetchResult.object(at: 0))
let imageData:Data = image.pngData()!
let base64String = imageData.base64EncodedString()
call.resolve([
"image": base64String
])
} else {
call.reject("Not authorised to access Photos")
}
})
}
}
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. Do this using the New file...
option when right clicking the App
folder in XCode.
Add the following code to App/PluginTest.m
#import <Foundation/Foundation.h>
#import <Capacitor/Capacitor.h>
CAP_PLUGIN(PluginTest, "PluginTest",
CAP_PLUGIN_METHOD(getLastPhotoTaken, CAPPluginReturnPromise);
)
NOTE: If you asked if you want to create a bridging header, do it. This allows Swift and Objective-C code to work together in iOS projects.
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 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';
const { PluginTest } = Plugins;
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
private lastPhoto: string = 'http://placehold.it/500x500';
constructor(public navCtrl: NavController, private domSanitizer: DomSanitizer) {
}
async getLatestPhoto(){
const result = await PluginTest.getLastPhotoTaken()
this.lastPhoto = "data:image/png;base64, " + result.image;
}
}
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 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 class="ion-padding">
<ion-button color="primary" expand="full" (click)="getLatestPhoto()"
>Get Latest Photo</ion-button
>
<img [src]="domSanitizer.bypassSecurityTrustUrl(lastPhoto)" />
</ion-content>
Modify home.scss to reflect the following:
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:
ionic build
and copy the web directory to your native project with:
ionic cap sync
you can then run:
ionic 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.