Displaying a Native List in Ionic

In the last article I published, How to Launch Native Views in an Ionic Application, I made a point about how it is awkward to use something like a native list in an Ionic application instead of the standard <ion-list>:

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.

Whilst for the general case I wouldn’t recommend attempting to use native lists with an Ionic application, there is an important distinction to make here. If, like the other native views we talked about in the previous tutorial, we launch the native list as a full screen overlay that sits above the web view with no need to integrate with it, then this approach can be feasible.

This is what we are going to build in this tutorial:

The plugin we will be building will allow us to call:

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

const { ListView } = Plugins;

ListView.present({
  items: [
    'My',
    'item',
    'array'
  ]
});

from within our Ionic application to launch a full screen native list on iOS (the plugin could also be extended to work for Android as well). This example doesn’t include sending data back to the Ionic application but, depending on what exactly it is you want to do, you might also extend this plugin such that a user could tap a particular list item and then the Ionic application would receive data indicating which item was tapped. Capacitor easily allows for this type of communication.

By launching a full screen native list, we don’t need to worry about the positioning of other elements in the Ionic application that lives inside of the web view. The use cases for this approach may be limited, but it could definitely fulfill a bit of a niche.

Lists with massive amounts of data can be one of the tricky things to pull off in an Ionic application. One of the biggest performance bottlenecks for a web-based application is having to deal with a large DOM (i.e. having lots of elements/nodes in your application). Naturally, if you want to display a list with hundreds or thousands of elements, you are going to have a large DOM.

Ionic does provide the ion-virtual-scroll component which helps a great deal with performance in this scenario since it recycles just a few DOM elements rather than having hundreds or thousands in the DOM at the same time, but there are a lot of limitations that come along with ion-virtual-scroll which can make it difficult to use. Generally, I would recommend using ion-infinite-scroll when dealing with large lists in Ionic so that you don’t need to load the entire list at once, but still, you might want to just have the entire massive list displayed all at once in some cases.

If ion-virtual-scroll does not suit your needs, nor does ion-infinite-scroll, then this is the case where perhaps using a native list view might suit your needs. Although this tutorial is geared toward Ionic developers, you might also not even be using Ionic with Capacitor at all and want to take this approach.

You can also watch the video overview of this tutorial below:

1. Creating the Plugin

For an overview of the general process of creating a plugin for Capacitor and installing that plugin locally, I would recommend reading through the previous tutorial or the documentation. This tutorial will primarily focus on the implementation details of the plugin.

If you prefer, it is also possible to build this functionality directly into your iOS project without needing to build a “proper” plugin as we did in this tutorial: How to run any native code in Ionic.

To create a new plugin, you can run the following command:

npx @capacitor/cli plugin:generate

2. Set up the TypeScript Interface

We will need to modify two files in the plugin to set up the API for the plugin. In this case, the plugin will provide a single present method that will accept an object containing an array of items as a parameter.

Modify src/definitions.ts to reflect the following:

declare module '@capacitor/core' {
  interface PluginRegistry {
    ListView: ListViewPlugin;
  }
}

export interface ListViewPlugin {
  present(options: { items: string[] }): void;
}

Modify src/web.ts to reflect the following:

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

export class ListViewWeb extends WebPlugin implements ListViewPlugin {
  constructor() {
    super({
      name: 'ListView',
      platforms: ['web'],
    });
  }

  present(options: { items: string[] }): void {
    console.log('PRESENT', options);
  }
}

const ListView = new ListViewWeb();

export { ListView };

import { registerWebPlugin } from '@capacitor/core';
registerWebPlugin(ListView);

3. Implement the Plugin in Swift

To implement the functionality for the plugin we will need to modify the Plugin.swift file in Xcode.

Open Plugin.xcworkspace in Xcode

Modify Plugin.swift to reflect the following:

import Foundation
import Capacitor

@objc(ListView)
public class ListView: CAPPlugin {

    @objc func present(_ call: CAPPluginCall) {
        let items = call.getArray("items", String.self) ?? [String]()
        let listView = TableViewController()
        listView.items = items
        
        DispatchQueue.main.async {
            self.bridge.viewController.present(listView, animated: true)
        }
        
    }
}

class TableViewController: UITableViewController {
    
    var items: [String] = [String]()
    
    override func viewDidLoad(){
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
        cell.textLabel?.text = items[indexPath.row]
        return cell
    }
    
}

We could split the code above into two distinct parts. First, we have all of the implementation details related to Capacitor inside of the ListView class. The present method will grab the items array that was passed in, create a new instance of TableViewController, and then display the controller by first grabbing a reference to the viewController used by Capacitor and then calling the present method on that (it is just coincidence that our method for the plugin is also called present, that has no particular relevance here).

Then if take a look specifically at the TableViewController class, we will find all of the implementation details for the list (UITableView) itself. There is nothing Capacitor related here, this is all just standard Swift code that you would expect to see used in a standard native application to display a list. That means you don’t need to search for any special “Capacitor” way to do this, you can just use the standard documentation for UITableViewController and UITableView, or you could make use of the hundreds of examples you will find at places like Stack Overflow.

To complete the functionality for this plugin, we need to make sure to expose this plugin to Capacitor.

Modify 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(ListView, "ListView",
           CAP_PLUGIN_METHOD(present, CAPPluginReturnNone);
)

4. Use the Plugin

Before we can use this standalone plugin in an actual application, we will need to install it through npm. If you do not want to publish your plugin to npm (which would allow you to just npm install the plugin like any other) you can also set it up locally. The previous tutorial contains details on how to do that.

Once you have the plugin installed, it is just a matter of getting a reference to the plugin through @capacitor/core:

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

const { ListView } = Plugins;

and then calling the present method with the items that you want to display in the list:

launchListView() {
  ListView.present({
    items: [
      "This",
      "is",
      "a",
    ],
  });
}

Summary

As I mentioned at the beginning of this tutorial, this approach may have its uses in special circumstances, but I would still default to just using a standard <ion-list> wherever possible. My main motivation in creating this tutorial was to demonstrate that an application built with Capacitor has access to everything a standard native application does, and if we get comfortable working in both these web and native contexts there are a lot of creative possibilities.

Check out my latest videos: