見出し画像

[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のアドレスバーがあることと、下部にツールバーがあることです。

画像1

「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」です。

画像2

まずみてもらいたいのは、青枠で囲っている中にある `Main.storyboard` です。Swiftプロジェクトでは、 `Main.storyboard` で画面レイアウトのハンドリングを行います。ですので、この中身を比較すると表示の大きな部分を把握することができます。Xcode上でGUIで表示しながらこの2つを比較しましょう。

まずは「New Swift Project」です。当たり前のことながらすべてがBlankとなっています。

画像7

それに対して、Capacitor/iOSのストーリーボードです。

画像8

こちらは、最初から `CAPBridgeViewController` が設定されており、View Controller全体がブリッジされている様子がわかります。ちなみにコードベースだと以下が設定されています。

<viewController id="BYZ-38-t0r" customClass="CAPBridgeViewController" customModule="Capacitor" sceneMemberID="viewController"/>

https://github.com/ionic-team/capacitor/blob/master/ios-template/App/App/Base.lproj/Main.storyboard#L14

では、この `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()
 }
...

> https://github.com/ionic-team/capacitor/blob/master/ios/Capacitor/Capacitor/CAPBridgeViewController.swift

1行目で、 `public` ファイルの中の変数 `startDir` (初期値は空)の `index` ファイルを、変数 `fullStartPath` に格納して、それがHTMLファイルであるかを判定しています。起点は `ios/App` となりますので、 `ios/App/public/index.html` がWebViewにロードされたことがわかります。

画像5

では、この `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」です。

画像6

こちらも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);
    }});
  }
}

> https://github.com/ionic-team/capacitor/blob/master/android-template/app/src/main/java/com/getcapacitor/myapp/MainActivity.java#L10-L21

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);
...

> https://github.com/ionic-team/capacitor/blob/master/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java#L63-L87

この中で `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);

> https://github.com/ionic-team/capacitor/blob/master/android/capacitor/src/main/java/com/getcapacitor/Bridge.java#L168-L202

少々当該メソッドが長いので割愛しましたが、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` となります。

画像6

おそらく多くの方は拡張子も見覚えがないと思いますが、 `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");
}

> https://github.com/ionic-team/capacitor/blob/master/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java#L17-L23

WebViewがロードされた時にこのメソッドが自動的に実行されることで共有空間を利用することができるようになります。Javaからは、WebViewに生えてる `JSObject()` メソッドによりその空間を利用することができます。

JavaScriptからは、Window関数以下に生成される `androidBridge.postMessage()` メソッドによりその空間を利用することができます。

プラグインのAndroidフォルダをみてみる

Androidプラグインの処理を書くのは `android/src/main/java/**/**/**.java` となります。

画像5

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プラグインは応答式にプラグインテンプレート構築に必要な情報を入力していきます。

ここから先は

11,813字 / 6画像

¥ 860

この記事が気に入ったらチップで応援してみませんか?