If we are building an iOS/Android application with Ionic and Capacitor, then the primary native view that we are using inside of the native application created by Capacitor is a web view that loads our Ionic application. A lot of the time we do almost everything inside of this web view and our Ionic application, but we are working inside of a standard native application and that means we can still use whatever native views/controls that we like.
This doesn’t mean we can just swap out all of our Ionic UI components for native controls - this isn’t feasible and would defeat the purpose of Ionic anyway. Consider that we might want to use a native list view control instead of the Ionic <ion-list>
to display a list in our application. The web view control that we are using displays web content and it takes up the full screen. We can’t put the native list view control inside of the web view with the rest of our web content, but we could put the list view on top of or behind the web view.
In most cases, this is still going to be ultimately useless/awkward because it is difficult to get our application in the web view to account for the positioning of the native control that is sitting on top of it - they are living in two different worlds. You could also have the native control behind the web view and have a portion of the web view be transparent (e.g. the <ion-content>
area) such that the native control behind it became visible, but we still have this positioning problem where part of the native list is going to be cut off by the content in our web view. There is likely some trickery you could use to pull this off (perhaps I’ll give this a go in a future tutorial), but it is likely more effort than it is worth (if you need to do this badly enough to justify the effort, then it probably means it would have made more sense to just build with native iOS/Android in the first place).
However, what we can do quite easily is launch native views that are designed to be launched over the top of an application. Things like the Camera, or an Augmented Reality viewer window, or modals like alerts, date pickers, toasts, and more. You could really do mostly anything, even a list view, as long as it is going to be occupying the whole screen or doesn’t need to worry about the positioning of content within the web view.
In this tutorial, we are going to walk through creating a Capacitor plugin to easily launch a native alert control. We will be using an alert as an example, but the same/similar concepts will apply no matter what native control it is that you are trying to display on top of the web view.
NOTE: Capacitor already has methods for displaying common native modals like alerts. We are walking through creating our own plugin solely to learn the mechanics of how launching native views work. We are using an alert modal as an example, but you could use this same process to trigger whatever you like natively.
1. Create the Capacitor Plugin
We are going to generate our own modular Capacitor plugin to do this which can easily be installed in both iOS and Android projects. This is the best practice way to do this, but if you are just interested in a quick/ad-hoc/platform specific way to implement native functionality you can check out this video: How to run any native code in Ionic.
To create the Capacitor plugin, run the following command:
npx @capacitor/cli plugin:generate
This will give us a plugin template to work with.
2. Define the TypeScript Interface
Next, we are going to modify the TypeScript definitions to suit the API of our plugin. We are just going to have a single method available called present
that can be passed a message
to display in the native alert.
Modify
src/definitions.ts
to reflect the following:
declaremodule'@capacitor/core'{interfacePluginRegistry{
Alert: AlertPlugin;}}exportinterfaceAlertPlugin{present(options:{ message:string}):void;}
Modify
src/web.ts
to reflect the following:
import{ WebPlugin }from'@capacitor/core';import{ AlertPlugin }from'./definitions';exportclassAlertWebextendsWebPluginimplementsAlertPlugin{constructor(){super({
name:'Alert',
platforms:['web'],});}present(options:{ message:string}):void{console.log('present', options);}}const Alert =newAlertWeb();export{ Alert };import{ registerWebPlugin }from'@capacitor/core';registerWebPlugin(Alert);
We won’t actually be creating a web implementation for this plugin, but we still need to make the appropriate changes to this file to prevent errors.
3. Implement the iOS Functionality with Swift/Objective-C
Now we need to implement the native code that will be triggered from our Ionic application. This code is what will handle launching the additional native view over the top of our native web view.
Open the
ios/Plugin.xcworkspace
file using Xcode
Modify
Plugin/Plugin.swift
to reflect the following:
importFoundationimportCapacitor
@objc(Alert)publicclassAlert:CAPPlugin{@objcfuncpresent(_ call:CAPPluginCall){let message = call.getString("message")??""let alertController =UIAlertController(title:"Alert", message: message, preferredStyle:.alert)
alertController.addAction(UIAlertAction(title:"Ok", style:.default))DispatchQueue.main.async {self.bridge.viewController.present(alertController, animated:true, completion:nil)}}}
We haven’t strayed too far from the default plugin template here, and we could call this function whatever we want and execute whatever kind of code we want, but the important part in the context of this tutorial is the usage of self.bridge.viewController
. In order to display a native view, we need to use the Capacitor Bridge to get access to get access to Capacitor’s view controller. It is this view controller that we need to use to present our native view which, in this case, is a native alert. You would use this same type of technique to display other native views in your application.
If you get the following error when opening the plugin in Xcode:
No such module 'Capacitor'
Make sure to run:
pod install
Inside of the ios
folder where the Podfile
is located. To expose this plugin to Capacitor, making it available to use from our Ionic application, we need to define the methods our plugin provides in the Objective-C file for our plugin.
Modify
Plugin/Plugin.m
to reflect the following:
#import<Foundation/Foundation.h>#import<Capacitor/Capacitor.h>// Define the plugin using the CAP_PLUGIN Macro, and// each method the plugin supports using the CAP_PLUGIN_METHOD macro.CAP_PLUGIN(Alert,"Alert",CAP_PLUGIN_METHOD(present, CAPPluginReturnNone);)
We have replaced the echo
method with our own present
method, and we have also changed the return type as well. Our method just launches a view and does not return anything, so we use CAPPluginReturnNone
instead of CAPPluginReturnPromise
. If your plugin provided additional methods, you would need to add them with addition CAP_PLUGIN_METHOD
definitions.
4. Implement the Android functionality with Java
As we just did for iOS, we will also need to implement some native Android code to handle displaying the native alert on Android.
Open the
android
folder in Android Studio
With Android Studio open, Find the .java
file for the plugin you created. I used a Plugin id
of com.joshmorony.plugins.alert
when creating the plugin with the generate command, so the .java
file is located at java/com.joshmorony.plugins.alert/Alert
.
Modify the plugins
.java
file to reflect the following:
packagecom.joshmorony.plugins.alert;importandroid.app.AlertDialog;importcom.getcapacitor.JSObject;importcom.getcapacitor.NativePlugin;importcom.getcapacitor.Plugin;importcom.getcapacitor.PluginCall;importcom.getcapacitor.PluginMethod;@NativePluginpublicclassAlertextendsPlugin{@PluginMethodpublicvoidpresent(PluginCall call){String message = call.getString("message");AlertDialog.Builder builder =newAlertDialog.Builder(this.bridge.getWebView().getContext());
builder.setMessage(message).setTitle("Alert");AlertDialog dialog = builder.create();
dialog.show();}}
We’ve included the necessary imports to create an alert using the Builder
method of AlertDialog
and, similarly to what we did for the iOS version of the plugin, we use Capacitor’s Bridge to get access to the context in which the web view for the application is running. We then use this context for launching our alert. Again, a similar technique would be used to display other types of native views.
5. Making the Plugin Available to an Application
Our plugin is complete, now we just need to use it. We should first build the plugin by running:
npm run build
Now we need to install this plugin into whatever application we want to use it in. To install the plugin in a local project without needing to publish it to npm
, you can run the following command inside of the directory for your plugin:
npm link
You can then install the plugin in your application using the following command inside of the directory for your application:
npm link plugin-name
adding then adding the path to the local plugin in the package.json
dependencies for your application:
"plugin-name":"file:../path/to/plugin-name",
This will allow you to install the plugin using npm
, but it will use the local files on your machine. To install the plugin now, just run:
npm install
WARNING: If you wish to later publish the plugin to npm
and use it from the npm registry instead, you must run npm unlink --no-save plugin-name
within your applications folder, and npm unlink
within your plugin project folder.
Now you should run the following command inside of the directory for your application:
npx cap sync
or
ionic cap sync
to sync everything to your native projects. There is one additional step for making the plugin available to use for Android.
Open up your project in Android Studio with:
npx cap open android
Add the following to
MainActivity.java
:
packageio.ionic.starter;importandroid.os.Bundle;importcom.getcapacitor.BridgeActivity;importcom.getcapacitor.Plugin;importjava.util.ArrayList;importcom.joshmorony.plugins.alert.Alert;publicclassMainActivityextendsBridgeActivity{@OverridepublicvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);// Initializes the Bridgethis.init(savedInstanceState,newArrayList<Class<?extendsPlugin>>(){{// Additional plugins you've installed go here// Ex: add(TotallyAwesomePlugin.class);add(Alert.class);}});}}
If you have made previous modifications to this file, make sure to keep them. The important parts in the code above are that we are importing our plugin:
importcom.joshmorony.plugins.alert.Alert;
and then we call the add
method to register the plugin:
add(Alert.class);
you can do this for as many plugins as you need.
6. Using the Plugin through Capacitor
We can now just use the plugin as we would any other Capacitor plugin to launch our native alert. Just set up a reference to the Alert
plugin wherever you want to use it:
import{ Plugins }from"@capacitor/core";const{ Alert }= Plugins;
and then call the present
method:
Alert.present({
message:"hello",});
Summary
A great deal of the benefit and power that Ionic provides is that most of what we are building is powered by web-tech - this has some limitations/drawbacks, but it is also a feature and benefit of Ionic. We generally benefit by keeping things in web land as much as possible, but if the situation calls for it we still have quite a lot of flexibility to use some native controls if required. An iOS/Android application created with Capacitor is still just a native application with all the features that come with it, and we can use those features as we see fit, we are just constrained by the fact that we have to work the web view into the equation.