はじめに
こんにちは、hacomonoでウェブアプリケーションの開発を担当しています、野崎です(社内ではサイモンと呼ばれています)。
少し前に、Nuxt 3とCapacitorで作るiOSアプリの記事をパブリッシュしており、今回はその続きです。Capacitorで構築したiOSアプリから外部ライブラリをプラグイン化して呼び出すという話をします。
併せて読んでいただけますと、より理解しやすくなりますのでそちらもご参考ください。
前提条件
hacomonoのPOSレジとは
hacomonoではフィットネスクラブやジムの店舗管理システムと連動して使えるPOSレジを開発、提供しています。
POSレジ自体のコンテンツはあくまでウェブアプリケーションとして構築されており、WebViewを介して画面操作が可能となっています。
iOSアプリ周辺のアーキテクチャを図にすると、以下のようになっています。
WebViewを入り口にウェブの世界の中でapiコールしたりライブラリの呼び出しを行ったりする、というのがこのPOSレジのざっくりしたアーキテクチャとなっています。
POSレジ本体はiPadにて稼働することのみを想定しており、利用する際はPOSレジ本体となるiPadと、レシートプリンターやバーコードリーダーといった周辺機器を接続した状態で連動する周辺機器(ハードウェア)を操作できる必要があります。
フルセットで組み立ててみると写真のようになります。よく見るレジをイメージしていただければ結構です。
ハードウェアのソフトウェアからの制御
弊社が推奨で連携しているスター精密社製ハードウェアは、ソフトウェアから制御できるようSDKを提供しています。
本記事で紹介する取り組みは、そのようなSDKが使える前提で実装をご紹介します。
この記事では扱わないこと
本記事では、Capacitorプラグインを作り、composablesとして導入する過程を主に説明するものです。そのため、中間プロセスで本筋から外れるものは紹介を割愛しております。
- 大前提として、ここでは手元にCapacitorを使ったiOSアプリのソースコードがあるものとします。そのため、Capacitorプロジェクトの立ち上げ方には触れておりません。
- Capacitorのプラグインを作成するにあたりiOSネイティブの実装を用意する必要もありますが、本記事の趣旨から外れるため詳しくは立ち入りません(別途実装される前提で進みます)。
- Capacitorプラグインをnpm packageにする作業は、ほとんどCapacitorのドキュメントに沿って進めているため簡易的な説明にとどめています。
ソフトウェアからハードウェアを制御する
POSレジはNuxt 3とCapacitorによるウェブアプリケーションとして構築され、また各種ハードウェアを操作できることが求められます。
すべてSwiftで実装されたiOSアプリであればハードウェアのSDK含めてSwiftの世界で完結するものの、主要な機能がすべてウェブアプリケーションとして実装されているため少し工夫が必要です。
今回は、Nuxt 3の外部ライブラリとして使えることもあり、Capacitorが提供しているプラグイン機能による拡張を採用しました。
Capacitorのプラグインを作り、Nuxt 3から呼び出すには3つのステップを踏みます:
- ハードウェアのSDKを呼び出すCapacitorプラグインを作成する
- Nuxt 3のプロジェクトに作成したCapacitorプラグインを導入する
- ハードウェア呼び出しをプラグインを使ってcomposablesにする
完了した際の構造は図のようになります。
それでは、一つずつ見ていきます。
ハードウェアのSDKを呼び出すCapacitorプラグインを作成する
CapacitorはNative APIをJavaScriptから操作できるようにするプラグインの機能を提供しています。
これにより、Capacitorを使ったプロジェクトに任意の機能を拡張できるようになります。
新しく作成するプラグインは、Swiftで書かれたハードウェア制御実装を、JavaScriptから操作するためのインターフェース機能を提供します。したがって、プラグイン内にはSwiftによる制御の実装本体とJavaScript向けにexposeされたJavaScriptコードを持つ一つのプロジェクトを別途構築します。
このプラグインライブラリがのちほどPOSレジ本体から呼び出されることとなります。
簡単に、実装の一部をご紹介します。基本の流れは、Capacitorが公開しているプラグイン作成ガイドにそって作業を進めることとなりますので、公式ドキュメントもご参考ください。
まずはドキュメントにある、公式のジェネレータを使ってプラグインの雛形を構築します。
対話形式のツールでコードを自動生成でき、作業が完了するとディレクトリができています。なお、iOSだけ必要な場合は android
を消してしまって問題ありません(実際弊社では消してしまっています)。
plugin ┣─ android/ ┣─ dist/ ┣─ example/ ┣─ ios/ ┣─ Plugin/ ┣─ Plugin.xcodeproj/ ┣─ Plugin.workspace/ ┣─ PluginTests/ ┣─ Pod/ ┣─ Podfile ┣─ node_modules/ ┣─ src/ ┣─ definitions.ts ┣─ index.ts ┣─ web.ts ┣─ package.json ┣─ tsconfig.json ... 中略
ios
にネイティブコード、 src
に typescript から利用するためのインターフェースを実装します。「はじめに」でも触れましたように、 ios
側のコードは本記事の趣旨と外れてしまうためここでは説明を割愛いたします。
それでは、まずはJavaScriptのほうから書いていきます。
今回は 「iPad に有線ないし Bluetooth 接続された スター精密製レシートプリンターを探す」メソッドを例に解説します。
はじめに、definitions.ts
にJavaScriptから呼び出すメソッドを定義しておきます。
export interface Plugin { searchPrinter(options: SearchPrinterOptions): Promise<SearchPrinterResult>; }
ジェネリクスはなにを与えても良いですが、ドキュメントにあるとおり Promise
でラップしたオブジェクトを返します。 また、スター精密のSDKを利用するので、利用のために必要なパラメータを引数 SearchPrinterOptions
として設定します。
あわせて、上記のメソッド定義に対応するネイティブコードも準備します。こちらにはボタンを押したらレシートを出すといったハードウェアを制御する振る舞いの実装本体がここに入ります(実装の詳細は割愛)。
@objc func searchPrinter(_ call: CAPPluginCall) { // SDKの呼び出しなど、ハードウェア制御の実装がここに来る call.resolve() }
ウェブアプリケーション側の実装を参考に、明示的に resolve
する必要があるようです。ドキュメントに指示がありますのでそれに従います。
最後に、JavaScriptのメソッド定義とそのメソッドにより呼び出されるメソッド実装の本体を関連付けるために、メインファイル(拡張子 .m
)に定義を追加します。
自動生成コードを使えばマクロが使える状態でメインファイルが用意されているので、定義したメソッドをCAP_PLUGIN
の引数で渡してあげればOKです。
#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(Plugin, "Plugin", CAP_PLUGIN_METHOD(searchPrinter, CAPPluginReturnPromise); )
JavaScriptのメソッド定義とSwiftのメソッド実装本体はCapacitorで間を取り持ってくれるため、利用者である我々の準備はここでいったんおしまいです。
Nuxt 3のプロジェクトに作成したCapacitorプラグインを導入する
ここまでで、Swiftの実装本体を呼び出すJavaScriptのインターフェースをプラグインとして準備できましたので、POSレジ本体の機能を提供するNuxt 3のウェブアプリケーションに導入します。
別のリポジトリなどでパッケージを作っておき、それをNuxt 3のほうにインポートする構築をしていきます。
Capacitorプラグインは自動生成コードから生成するとnpm package公開も含めたビルドスクリプトなども用意してくれます。
ドキュメントのワークフローに乗って、変更都度ビルドし、npm packageとして配布できるようにしておきます。
Capacitorのプラグインは上述の通りテンプレートを用意しており、その中にはnpm packageとしてpublishできる各種コマンドなども含まれています。Capacitorの標準スクリプトに沿って作業を進められ、特にカスタマイズもしていないため管理方法の説明は割愛します。
さきほど作成したプラグインの index.ts
がNuxt 3から参照できるようになっていれば導入は完了です。
ハードウェア呼び出しをプラグインを使ってcomposablesにする
Nuxt 3のプロジェクトにCapacitorのプラグインが導入できましたので、あとは操作ロジックをどこに定義してどう呼ぶかを決めれば完了です。
ここでもいくつかの選択肢がありますが、はじめのステップで示した通りcomposablesにしていきます。Nuxt 3では、 composabes
配下に置くことで auto-imports を使った自動インポート対応の恩恵も受けられます。
composablesはVue.js 3のComposition APIを前提とした、UIやコンポーネントに依存しない振る舞いを定義するオブジェクトです。Vue.js 3のドキュメントには以下のようにあります:
「コンポーザブル(composable)」とは、Vue の Composition API を活用して状態を持つロジックをカプセル化して再利用するための関数です。
レシートを出したりキャッシュドロワーを開けたりする、ソフトウェアからのハードウェア操作はUIやコンポーネントに依存しない振る舞いなので、その観点でcomposablesとして状態および振る舞いを一箇所にバンドルしておくのはVue 3/Nuxt 3において望ましい設計であると考えられます。
ハードウェア側から提供されるSDKをコールするハードウェア操作の実装本体はプラグインとして切り出されているため、composablesにすることでハードウェア操作の実装本体とビジネスロジックを明確に分離しやすくなるのも利点です。よくある、domain層とinfrastructure層に関心を分離するのに近い設計感となります。
composabelsの実装を簡単にご紹介します。
composabelsは再利用性のあるロジック置き場であるので、ハードウェアを操作するメソッドの定義とexportを準備していきます。
先程まで例に使っていた、初期化時に使えるレシートプリンタを探すメソッドであれば次のように書くことができます。 実際にはもう少し色々ぶらさがっているのですが、実装例ということで大枠のみ示しております。
import { default as StarPrinter, type SearchPrinterOption, type SearchPrinterResult } from 'capacitor-star-printer' export interface UsePrinter { searchPrinter(option: SearchPrinterOption): Promise<SearchPrinterResult | void> } export function usePrinter(): UsePrinter { return { searchPrinter(opt) { if (process.client) { return StarPrinter.searchPrinter(opt) } } } }
Nuxt 3のアプリケーションはSSRモードを使っており、WebViewを介してブラウザとして操作される前提のアプリケーションです。そのため、process.client
を使ってサーバサイド(Node.js)での実行ではないことを確認しています。
厳密にはPCブラウザもネイティブ実装を呼び出せないためチェックが必要ですが、WebViewを介してNuxt 3のアプリケーションにアクセスする前提なので省略しています。
返している高階関数内で使っている StarPrinter
クラスがプラグインの状態や振る舞いを持っており、composablesなど使う側で呼び出すことができるようになっています。具体的な定義はプラグイン側の index.ts
およびそれに対応するSwiftコード内にあるものです。
あとはそのままuse関数で返してあげればよいです。
以上で、コンポーネントのほうからcomposablesを経由してプラグイン、そしてハードウェア操作をコールできるようになりました。
はじめ、ハードウェアの制御をJavaScriptからできるのかなと半信半疑でしたが、フレームワークの思想に則って順番に作ってみるとそう難しくない手順で組み込めました。
おわりに
Nuxt 3とCapacitorを組み合わせる実例が少なく、調べてもほしい情報に行き当たらないこともあるかもしれません。Capacitorの事例にあまり行き当たらないですよね。
本記事でご紹介した内容は、ハードウェアとの連携という少しニッチなものでしたが、なにかご参考になれば幸いです。