はじめに
テックブログ初投稿となります、野崎です(社内ではサイモンと呼ばれています)。 ウェブアプリケーションの開発を担当しております。
hacomonoではフィットネスクラブやジムの店舗管理システムと連動して使えるPOSレジを開発、提供しています。
このPOSレジはiOS、特にiPadでの動作をターゲットに開発されていますが、チームには専属のアプリエンジニアが1名しかおらず、自分含めみなウェブアプリケーションエンジニアです。 そのような条件から、iOSアプリの開発はウェブを中心に据える必要がありました。本記事では、クロスプラットフォームライブラリであるCapacitorをNuxtと組み合わせ、”ほぼウェブな”iOSアプリ開発をご紹介します。
以後、本文に登場するコードのフルセットはgitリポジトリにてご覧になれます。
最低限の実装にいくつか実装を追加しているため、全く同じ見た目ではない前提でご参考いただければ幸いです。
詳しくは後述しますが、サンプルコードのリポジトリではnpx cap sync
などで誤って上書きしないよう、Capacitorによるswiftの自動生成コードは capacidor-ios
とリネームしています。
技術選定と要件
技術選定の観点
Capacitor(キャパシター)はIonicベースのクロスプラットフォームフレームワークです。Vue.jsやReactとといった人気のウェブアプリケーションフレームワークと組み合わせて、簡単にネイティブアプリを構築することができます。 選定当時、いくつかの要件をあげ、検証を行った結果Capacitorがよいという判断になりました。選定要件には主に次の4つがあります:
1.ハードウェアと連携できること
POSレジは、その機能を提供するためにレジの本体であるiPadアプリだけでなくレシートプリンター、バーコードリーダー、キャッシュドロワーといった外部ハードウェアとシームレスに連携できる必要があります。 これらのハードウェアはSDKが提供されており、ソフト側から制御できるもののウェブアプリケーションとの連携においては大なり小なりの苦労が伴います。検証段階にてそのようなハードウェアと問題なく連携できるかが問われておりました。
2. チームにはウェブアプリケーションエンジニアしかいなかったこと
現在でこそアプリエンジニアの方に参画いただけているものの、POSチーム発足当時はウェブアプリケーションエンジニアしかいませんでした。そのため、アプリケーションの開発はウェブに完結したかった事情があります。
3. デザインシステムの構築を推進していること
hacomonoで提供している各プロダクト間で共通言語となるデザインシステムの構築を進めています。ウェブアプリケーションエンジニア中心のチームで今後もウェブサービスの提供が中心となるため、うかつにiOSの画面実装を増やしたくない、という狙いが当時からありました。
4. iOSアプリ内部でバックエンドapiの環境を動的に切り替えたい
この要件は社内固有の事情に依るのですが、hacomonoのウェブアプリケーションはURLを使って環境を識別しています。そのためiOSアプリ側からも「どの環境を使うか」を設定値として入力する機構が必要でした。
補足: ウェブアプリケーション部分
iOSアプリで表示するウェブアプリケーションの部分も概観しておきます。 iOSアプリからは、WebViewでウェブアプリケーションを表示することを目指しました。hacomonoではPOSレジ以外のプロダクトでもNuxtが使われており、レジ機能のコアになる機能はNuxtで構築しています。ウェブサービスの全体は、このNuxtで構築されたウェブフロントエンドから各種バックエンドapiに通信するアーキテクチャになっています。
プロジェクトにCapacitorを導入する
Capacitorは多くの人にとって馴染みのないかと思いますので、簡単にドキュメントに沿った導入方法を確認します。日本語のドキュメントも非常に充実していますので、詰まることなく作業を進められます。
前提条件
手元に構築済みのウェブアプリケーションがあり、yarnやnpmといったパッケージマネージャが使えるものとします。
既存のプロジェクトにCapacitorを導入する
公式ドキュメントの「既存のウェブアプリケーションにCapacitorを追加する」に沿って順番に作業します。
npm i @capacitor/core npm i -D @capacitor/cli # capacitor設定を初期化 npx cap init # iOSのライブラリを追加 npm i @capacitor/ios # iOSプロジェクトを作成する npx cap add ios
npx cap add ios
を終えるとルート配下に ios
ディレクトリが作成され、中にはUIKitベースのプロジェクトができていることが確認できます。ここから自動生成コードを要件に合うようにカスタマイズしていきます。
自動生成コードの確認
ios
ディレクトリにCapacitorが生成したコードができていると述べました。生成時点でできるコードは以下のようになっており、UIKitベースの実装であることがわかります。
- Base.lproj
- Main.storyboard
- スプラッシュの次のWebViewを生成するstoryboardです
- このstoryboardには、Capacitorのネイティブライブラリに含まれる
CAPBridgeViewController
が紐付けられています。
- LaunchScreen.storyboard
- デフォルトで作成されるスプラッシュです。
- Main.storyboard
- AppDelegate
- main関数相当のDelegateです。
UIApplicationDelegate
を継承。
- main関数相当のDelegateです。
SwiftUIベースの、できるだけ画面を持たない構成に修正していく
iOSアプリのUI実装は、SwiftUIとUIKitを組み合わせて実現します。iOSアプリ開発のトレンドはSwiftUIに向かいつつも、それだけでは不便なケースが多く、CapacitorのネイティブライブラリもUIKitにより構成されています。
要件を満たすべく、自動生成コードからstoryboardを使った画面の構成を避けてSwiftUIベースのWebViewを構築します。
Capacitorのネイティブライブラリは CAPBridgeViewController
がウェブとネイティブをつなぐ役割を担っているため、カスタマイズを施してViewControllerをSwiftUI/Viewと組み合わせて使える状態を目指します。
はじめに、作業後のファイル構成例を役割ごとに示します。
- エントリポイント
- WebApp.swift
App
を継承した構造体。アダプタを挟んでAppDelegate
をSwiftUIで使えるようにします。
- WebApp.swift
- WebViewの構築準備
- MainView.swift
WebViewControllerRepresentable
を返す中間View。後述。
- EnvironmentView.swift
- 通信する環境のURL指定を促します。サンプルではViewとして作ってしまいましたがウェブの構築にあたって必要な入力が得られれば普通のアラートなどでも問題ありません。
- ViewControllerにわたすインプットの受け皿であればなんでもよいクラスです。
- MainView.swift
- View/ViewController
- CDBridgeViewController.swift
- Capacitorのネイティブライブラリが用意している
CAPBridgeViewController
を継承したViewControllerクラス。 - hacomonoのPOSレジのように、カスタマイズするユースケースも想定されており公式ドキュメントに沿ってカスタムコードを追加します。
- Capacitorのネイティブライブラリが用意している
- WebViewControllerRepresentable.swift
- ViewControllerからSwiftUI/Viewを生成するための中間表現となる構造体。
UIViewControllerRepresentable
に準拠することで、手動でnewしたViewControllerからViewを生成できるようにします。
- ViewControllerからSwiftUI/Viewを生成するための中間表現となる構造体。
- CDBridgeViewController.swift
- ユーティリティ
- AppRuntimeState.swift
- URLなどなにがしか受け付けた入力を持っておくクラスです。
ObservableObject
を継承しています。
- URLなどなにがしか受け付けた入力を持っておくクラスです。
- AppRuntimeState.swift
それでは、主要なクラス・構造体のソースコードを紹介します。ここに示すサンプルコードは、ポイントが見えやすいよう必要最低限の記載にとどめております。Viewのプレビューも外しています。
WebApp
はCapacitorが生成するAppDelegateをSwiftUIで使えるようにする構造体で、いわゆるmain関数の働きをします。ポイントは UIApplicationDelegateAdaptor で、UIKitのAppDelegate
を生成するプロパティラッパになります。ここから、任意の前処理を行うViewなどを返します。
import SwiftUI @main struct WebApp : App { @UIApplicationDelegateAdaptor (AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { EnvironmentView().environmentObject(AppRuntimeState()) } } }
WebApp.swift
WebApp
から推移してくる最初のViewです。UIViewControllerの生成、WebView表示前にやっておきたい前処理などを挟む役割を想定しており、さらに責務を小さくするならViewではなくアラート主体のControlllerに置き換えてもよいです(POSレジではそのようになっています)。
import SwiftUI import Foundation struct EnvironmentView: View { @State private var envString = "" @EnvironmentObject var appRuntimeState: AppRuntimeState var body: some View { NavigationView { VStack { Button( action: { self.appRuntimeState.isEnvironmentValid = true }, label: { Text("GO") } ) NavigationLink( destination: MainView(hostUrl: self.$envString.wrappedValue), isActive: $appRuntimeState.isEnvironmentValid) { EmptyView() } } } } }
EnvironmentView.swift
ここからWebViewの生成処理です。 MainView
は WebViewControllerRepresentable
を経由してWebViewを表示します。
import SwiftUI struct MainView: View { var hostUrl: String = "" var body: some View { VStack { WebViewControllerRepresentable(hostUrl: self.hostUrl) } } }
MainView.swift
本カスタマイズ一番のポイントは WebViewControllerRepresentable
といえます。UIViewControllerRepresentable はSwiftUI内でUIKitのViewControllerを生成するためにインターフェースとなるプロトコルです。
すでに触れたように、Capacitorのネイティブライブラリは CAPBridgeViewController
でビューの生成にまつわる様々な処理をラップしています。
したがって、CAPBridgeViewController
を継承したViewControllerからViewを作れるようになる必要があり、WebViewControllerRepresentable
はUIKit/ViewControllerとSwiftUI/Viewの変換を担うクラスとなります。
import Foundation import SwiftUI struct WebViewControllerRepresentable : UIViewControllerRepresentable { var hostUrl: String = "" func makeUIViewController(context: Context) -> some UIViewController { let viewController = CDBridgeViewController() viewController.hostUrl = self.hostUrl return viewController } func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } }
WebViewControllerRepresentable.swift
WebViewControllerRepresentable
で生成したViewControllerの内部実装です。ここで、WebViewの生成をカスタマイズします。なお、 viewDidLoad
内に登場する変数 webView
はWKWebView
のインスタンスで、インスタンスの生成自体は CAPBridgeViewController
にて行われています。つまり、サブクラスからみるとスーパー参照の構造になります。
import Capacitor import UIKit import Foundation class CDBridgeViewController: CAPBridgeViewController { var hostUrl: String = "" override func viewDidLoad() { super.viewDidLoad() self.becomeFirstResponder() guard let u = URL(string: self.hostUrl) else { return; } webView?.load(URLRequest(url: u)) } }
CDBridgeViewController.swift
登場したクラスや構造体を一部端折ってまとめますと図のようになります。 以上のような実装を踏まえることでstoryboardを廃してSwiftUIとCapacitorネイティブライブラリを組み合わせて利用することができるようになりました。
おまけ: Capacitorとハードウェアの連携
冒頭で触れたように、POSレジは外部ハードウェアとケーブルによって接続して動作します。Capacitorはプラグイン用のapiを公開しており、独自のプラグインを作成することができます。ハードウェアの制御を担うソフトウェアはプラグインとして開発し、このプラグインをウェブアプリケーションから呼び出す形式を採用しています。
おわりに
iOSアプリ対応だけではなく、ハードウェアとの連携やウェブに重心を置く開発も踏まえた技術選定は外部公開されている事例も少なく、参考にできる情報が少ないのが実情です。
取り組みの一部紹介ではありますが、ご参考いただければ幸いです。