How to Launch Native Views in an Ionic Application

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:

declare module '@capacitor/core' {
  interface PluginRegistry {
    Alert: AlertPlugin;
  }
}

export interface AlertPlugin {
  present(options: { message: string }): void;
}

Modify src/web.ts to reflect the following:

import { WebPlugin } from '@capacitor/core';
import { AlertPlugin } from './definitions';

export class AlertWeb extends WebPlugin implements AlertPlugin {
  constructor() {
    super({
      name: 'Alert',
      platforms: ['web'],
    });
  }

  present(options: { message: string }): void {
    console.log('present', options);
  }
}

const Alert = new AlertWeb();

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:

import Foundation
import Capacitor

@objc(Alert)
public class Alert: CAPPlugin {

    @objc func present(_ 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:

package com.joshmorony.plugins.alert;

import android.app.AlertDialog;

import com.getcapacitor.JSObject;
import com.getcapacitor.NativePlugin;
import com.getcapacitor.Plugin;
import com.getcapacitor.PluginCall;
import com.getcapacitor.PluginMethod;

@NativePlugin
public class Alert extends Plugin {

    @PluginMethod
    public void present(PluginCall call) {
        String message = call.getString("message");

        AlertDialog.Builder builder = new AlertDialog.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:

package io.ionic.starter;

import android.os.Bundle;

import com.getcapacitor.BridgeActivity;
import com.getcapacitor.Plugin;

import java.util.ArrayList;
import com.joshmorony.plugins.alert.Alert;

public class MainActivity extends BridgeActivity {
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Initializes the Bridge
    this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
      // 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:

import com.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.

Check out my latest videos: