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:
Outline
Source code1. 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.