[Ionicモバイルアプリ制作レシピ] ウェブアプリのクロスプラットフォーム化を実現するCapacitorの仕組みと、ウェブからSwift/Javaのコードを実行する方法
はじめに
Capacitorは、クロスプラットフォームのためのライブラリで、ウェブアプリを、ウェブ上ではもちろんのこと、iOS、Android、Electronでネイティブに動作させることができ、あなたのウェブアプリを「App Store」や「Google Play」で配信することができます。
このことで、ウェブブラウザだけでは実装できないFaceID APIや指紋認証API、Health APIへのアクセス、プラットフォーム独自の課金APIの利用を行うことができます。iOSではSwift、AndroidではJavaの任意のコードを実行することもできますので、あなたのアプリのユーザ体験を飛躍的に向上させることができます。
本章では、Capacitorの仕組みを概念と実装の両面から把握したあと、「Firebase Crashlytics」を使ったCapacitorプラグインをチュートリアル形式でつくっていくことでクロスプラットフォームの任意のコードの実装への理解を深めます。
本記事は、「Ionicで作る モバイルアプリ制作入門[Angular版]」のネクストステップとして、よりCapacitorへの理解を深めるために提供しています。
そのため、Capacitorを使うための環境構築やビルド方法などは書籍をご購入いただくか、Capacitor日本語ドキュメンテーションをご確認いただきますようお願い申し上げます。
本記事は後半のチュートリアルを有料記事としてだしています。初心者向けではなく、こういう本気の記事はどれほどニーズがあるかを確認したいと思っていまして、「中級者以上向けの記事を読みたい」という方はぜひご購入いただけましたら幸いです。
1. Capacitorが動く仕組み
Capacitorがどのように動くかをみていきましょう。Capacitorには大きく2つの仕組みをもっています。ひとつは「アプリを表示する」、もうひとつは「WebからSwiftやJavaのコードを実行する」です。これらについてどのように実現しているかを追っていきます。
1.1. Capacitorがアプリを表示する仕組み
モバイルデバイス(iPhoneやAndroid)で、ストアからインストールするアプリと、ブラウザで表示するWebサイトの違いについて外観から考えてみます。違いは、たった2点、上部にURLのアドレスバーがあることと、下部にツールバーがあることです。
「iOSっぽいデザインをしている」「Material Designに沿ってUIがつくられている」というのは、ただのデザインの問題で、Webサイトでもルールに沿ってデザインすれば同様にデザインを実現可能です。iOSでいうプッシュ遷移やナビゲーションスタックを実現するモバイルデザインフレームワーク「Ionic Framework」などを使えば、Webでも容易に実現が可能です。
ですので、つまりはアドレスバーとツールバーを取り去って縦横100%に表示することができれば、Webアプリをまるでネイティブアプリのようにユーザに提供することが可能となります。それを実現しているのがCapacitorです。
それでは、具体的にCapacitorがどのように実現しているかを追っていきましょう。Capacitorはnpmパッケージとして公開されており、既存アプリにインストールするためには
% npm install @capacitor/core @capacitor/cli
で必要なパッケージをインストールして、
% npx cap init
でCapacitorの初期化を行うことができます。その後、 `npx cap add ios` コマンドで `ios/` ディレクトリ以下にiOSアプリとしてのファイル一式、 `npx cap add android` で `android/` ディレクトリ以下にAndroidアプリとしてのファイル一式を生成することができます。
1.1.1. iOSで表示する仕組み
`npx cap add ios` を実行して生成された `ios/` ディレクトリの中身を追っていきます。Capacitor/iOSの特徴を把握するために、以下で、Xcodeの「New Swift Project」から作成したブランクのプロジェクトとフォルダ構成を比較しました。
左がCapacitor/iOS、右が「New Swift Project」です。
まずみてもらいたいのは、青枠で囲っている中にある `Main.storyboard` です。Swiftプロジェクトでは、 `Main.storyboard` で画面レイアウトのハンドリングを行います。ですので、この中身を比較すると表示の大きな部分を把握することができます。Xcode上でGUIで表示しながらこの2つを比較しましょう。
まずは「New Swift Project」です。当たり前のことながらすべてがBlankとなっています。
それに対して、Capacitor/iOSのストーリーボードです。
こちらは、最初から `CAPBridgeViewController` が設定されており、View Controller全体がブリッジされている様子がわかります。ちなみにコードベースだと以下が設定されています。
<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>
では、この `CAPBridgeViewController` とは何者でしょうか。追ってみると、実は生成したCapacitor/iOSプロジェクト内には該当するController Classは存在しません。そこで、Swiftのライブラリ管理ツール「CocoaPods」のライブラリ指定をしているファイル「Podfile」( `ios/App/Podfile` / JavaScriptでいう `package.json` みたいなものです)を確認してみましょう。
platform :ios, '11.0'
use_frameworks!
# workaround to avoid Xcode 10 caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
# Automatic Capacitor Pod dependencies, do not delete
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
# Do not delete
end
...
https://github.com/ionic-team/capacitor/blob/master/ios-template/App/Podfile
インストールするパッケージのpathに `node_modules` 内のパッケージが指定されているのが大きな特徴です。Capacitor/iOSはウェブファーストで設計されているので、中身はCocoaPodsのライブラリとしてではなく、npmパッケージとして公開されています。また、このことにより、CocoaPodsとnpmの二重管理になることを回避しています。
ですので、 `CAPBridgeViewController` は `node_modules/@capacitor/ios` 内にある `CAPBridgeViewController.swift` で定義されています。ここではすべてのコードを追うのは割愛しますが、表示に重要な役割を担っているのは、 `loadWebView()` メソッドです。
func loadWebView() {
let fullStartPath = URL(fileURLWithPath: "public").appendingPathComponent(startDir).appendingPathComponent("index")
if Bundle.main.path(forResource: fullStartPath.relativePath, ofType: "html") == nil {
fatalLoadError()
}
...
1行目で、 `public` ファイルの中の変数 `startDir` (初期値は空)の `index` ファイルを、変数 `fullStartPath` に格納して、それがHTMLファイルであるかを判定しています。起点は `ios/App` となりますので、 `ios/App/public/index.html` がWebViewにロードされたことがわかります。
では、この `public` に入っているのは何者でしょうか。
Capacitorを最初にインストールした時に、 `npx cap init` を行いますが、それによって自動的に `capacitor.config.json` が生成されます。このファイルで様々なCapacitorの設定を行うことができますが、キー `webDir` に指定されているフォルダが、そのまま `npx cap add` もしくは `npx cap copy` コマンドによって `public` としてコピーされます。
この一連の処理によって、iOSは縦横100%のWebViewを用意し、ウェブアプリを表示しています。
1.1.2. Androidで表示する仕組み
続いて `npx cap add android` を実行して生成された `android/` ディレクトリの中身を追っていきます。
Capacitor/Androidの特徴を把握するために、以下で、Android Studioの「New Java Project」から作成したブランクのプロジェクトとフォルダ構成を比較しました。
左がCapacitor/Android、右が「New Java Project」です。
こちらもiOS同様に似たフォルダ構成になっており、CapacitorはWebViewを用意しているだけで本質的にはネイティブとプロジェクト構成は変わらないことがわかります。
Javaでは、 `MainActivity.java` で最初の読み込みや画面レイアウトのハンドリングを行いますので、このファイルの中身を確認します。
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);
}});
}
}
classを確認すると、 `BridgeActivity` をextendsしていることがわかりますのでこちらを追います。 `BridgeActivity` は `BridgeActivity.java` で定義されており、この中の `load` メソッドで呼び出しをしていることがわかります。
protected void load(Bundle savedInstanceState) {
Logger.debug("Starting BridgeActivity");
webView = findViewById(R.id.webview);
cordovaInterface = new MockCordovaInterfaceImpl(this);
if (savedInstanceState != null) {
cordovaInterface.restoreInstanceState(savedInstanceState);
}
mockWebView = new MockCordovaWebViewImpl(this.getApplicationContext());
mockWebView.init(cordovaInterface, pluginEntries, preferences, webView);
pluginManager = mockWebView.getPluginManager();
cordovaInterface.onCordovaInit(pluginManager);
bridge = new Bridge(this, webView, initialPlugins, cordovaInterface, pluginManager, preferences);
...
この中で `new Bridge` している部分がWebViewの中身を指定しているコードですので、更に class `Bridge` を追います。
private void loadWebView() {
appUrlConfig = Config.getString("server.url");
String[] appAllowNavigationConfig = Config.getArray("server.allowNavigation");
...
localServer = new WebViewLocalServer(context, this, getJSInjector(), authorities, html5mode);
localServer.hostAssets(DEFAULT_WEB_ASSET_DIR);
...
webView.loadUrl(appUrl);
少々当該メソッドが長いので割愛しましたが、WebViewを用意し、ローカルサーバを立ち上げてWebViewからアクセスしている様子がわかります。
この一連の処理によって、Androidは縦横100%のWebViewを用意し、ウェブアプリを表示しています。
1.1.3. Capacitorがアプリを表示する仕組みのまとめ
Capacitorは、WebViewを用意し、必要に応じてローカルサーバを立ち上げてWebViewからアクセスすることによって、iOS/Androidでウェブアプリをネイティブアプリとしてみせていることがわかりました。
この仕組さえわかっていれば、よくある以下のような批判に対して技術的なコメントを行うことができます。
WebViewアプリはビルドに失敗しやすい
依存性を持ち合わせずにWebViewを表示しているだけなので、ビルドの失敗のしやすさでいえばWebViewアプリであるか否かは影響しないことがわかります。
WebViewアプリは起動が遅いので使い物にならない
ローカルにコピーしたウェブアセット一式を表示しているにすぎないので、仮にこれが使い物にならないなら、Safariなどのモバイルウェブブラウザすべてが使い物にならないことになります。
WebViewアプリはNativeアプリに比べて遅い
WebViewの起動(Androidの場合はローカルサーバの起動)を待つコストがあるので、起動が速くないのはその通りです。けれどファイルへのアクセスはネットワーク越しではなくローカルホストにあるため、モバイルWebブラウザと比較すると通信コストは不要となります。
1.2. Capacitorプラグインが動く仕組み
前項「Capacitorがアプリを表示する仕組み」では、ウェブアセットを表示するまでの一連の流れをみてきました。本項では、JavaScriptとデバイスのネイティブ言語(Swift/Java)とがどのように通信するかを追っていきましょう。
Capacitorでは、ビルドしたファイルに直接コードを書き足して通信する方法と、専用プラグインをつくってそれを介して通信する方法がありますが、ここではより汎用的に使える後者の仕組みからみていきます。
以下のコマンドでプラグインテンプレートを生成できます。
% npx @capacitor/cli plugin:generate
クロスプラットフォーム専用のプラグインですので、このコマンドひとつで、ウェブ、iOS、Androidのそれぞれのメソッドを書くことができるテンプレートが生成されます。
1.2.1. iOSで動く仕組み
iOSで、JavaScriptとSwiftをつないでいるのは `JSExport.swift` にある以下のメソッドです。
public static func exportJS(userContentController: WKUserContentController, pluginClassName: String, pluginType: CAPPlugin.Type) {
var lines = [String]()
lines.append("""
(function(w) {
w.Capacitor = w.Capacitor || {};
w.Capacitor.Plugins = w.Capacitor.Plugins || {};
var a = w.Capacitor; var p = a.Plugins;
...
let js = lines.joined(separator: "\n")
let userScript = WKUserScript(source: js, injectionTime: .atDocumentStart, forMainFrameOnly: true)
userContentController.addUserScript(userScript)
}
> https://github.com/ionic-team/capacitor/blob/master/ios/Capacitor/Capacitor/JSExport.swift#L51-L78
これが、WebViewがロードされた時に自動的に実行されることで、 `WKScriptMessageHandler` プロトゴルを実装しています。専門的になりすぎるので簡単に説明すると、JavaScriptとSwiftで値やイベントフックを共有できる空間をつくって、そこを経由して値やイベントフックの受け渡しを行います。
Swiftからは、WebViewに生えてる `evaluateJavaScript()` メソッドによりその空間を利用することができます。JavaScriptからは、Window関数以下に生成される `webkit.messageHandlers.bridge.postMessage()` メソッドによりその空間を利用することができます。
そしてこれを仲介するためにプラグインを利用します。
プラグインのiOSフォルダをみてみる
CapacitorはiOSプラグインをSwiftで書きますので、主に処理を書くのは `ios/Plugin/Plugin.swift` となります。
おそらく多くの方は拡張子も見覚えがないと思いますが、 `Plugin.h` ファイルと `Plugin.m` がセットで生成されます。これにより、先程つくった空間と `Plugin.swift` の各メソッドを紐つけます。
プラグインはSwiftプラグインと同様の挙動で読み込まれますので、Swiftプラグインとして必要な `Info.plist` や一式のファイルがデフォルトで用意されています。プラグイン毎に、Swiftのライブラリ管理ツール「CocoaPods」を管理することができるので、外部ライブラリに依存したプラグインを書くこともできます。
1.2.2. Javaで動く仕組み
Javaは、もともとWebViewと簡単に値やイベントハンドラを共有できる `JavascriptInterface` というAPIをもっており、これを利用しています。
ですので、iOSと比べて登録メソッドはとても簡潔で、 `MessageHandler.java` で以下のようにAPIが登録されています。
public MessageHandler(Bridge bridge, WebView webView, PluginManager cordovaPluginManager) {
this.bridge = bridge;
this.webView = webView;
this.cordovaPluginManager = cordovaPluginManager;
webView.addJavascriptInterface(this, "androidBridge");
}
WebViewがロードされた時にこのメソッドが自動的に実行されることで共有空間を利用することができるようになります。Javaからは、WebViewに生えてる `JSObject()` メソッドによりその空間を利用することができます。
JavaScriptからは、Window関数以下に生成される `androidBridge.postMessage()` メソッドによりその空間を利用することができます。
プラグインのAndroidフォルダをみてみる
Androidプラグインの処理を書くのは `android/src/main/java/**/**/**.java` となります。
Javaは各プラグインがパッケージ名をもっており、それに応じたフォルダが設定されます。例えば、上記例だと、パッケージ名を `jp.rdlabo.hello` にしたため、フォルダ構成が `android/src/main/java/jp/rdlabo/hello/` となっています。Androidプラグインはメソッド名の紐付けなどは不要なので、HelloPluginに直接コードを書くと実行することができます。
プラグイン毎に、ビルドシステム「Gradle」をもっており、そこで外部ライブラリを読み込むことができるので、外部ライブラリに依存したプラグインを書くこともできます。
1.2.3. Capacitorプラグインが動く仕組みのまとめ
`WKScriptMessageHandler` プロトゴルと `JavaScript Interface` APIを利用することによって、JavaScriptとSwift、もしくはJavaScriptとJavaの連携は行うことができるようになっています。またCapacitorはそれを「プラグイン」という仕組みで連携や開発を行う仕組みをもっています。
また、SwiftではCocoaPods、JavaではGradleを利用することで外部ライブラリを利用することもできますので、コアプラグインや現在リリースされているコミュニティプラグインに限定されず、自分で容易にプラグインを開発して取り込むことができるのは大きな特徴のひとつです。
2. チュートリアル「Firebase Crashlyticsプラグインをつくってみよう」
それでは実際にプラグインをつくってみましょう。
実務ではよく外部ライブラリと組み合わせてプラグインを開発しますので、ここでは「Firebase CrashlyticsのCapacitorプラグイン化」を行います。Crashlyticsは、アプリのクラッシュレポートをFirebase上で管理、確認することができるツールです。
2.1. plugin:generate
それでは、まずプラグインテンプレートを作成します。以下のコマンドを実行してください。
% npx @capacitor/cli plugin:generate
Capacitorプラグインは応答式にプラグインテンプレート構築に必要な情報を入力していきます。
ここから先は
¥ 860
この記事が気に入ったらチップで応援してみませんか?