ABEJA Tech Blog

中の人の興味のある情報を発信していきます

今年書いたネイティブコードが全部 Objective-C だった件 ~転生したら Swift 書く~

こちらは ABEJA アドベントカレンダー2022 の 5 日目の記事です。

はじめに

こんにちは。CS 統括部システム開発グループ 1 の石川 (@ishikawa) です。

9 月に ABEJA は、本社オフィスを「Bizflex 麻布十番」に移転しました2。本社移転を受けて、Bizflex オフィスを利用する社員も増えています。そして、Bizflex のサービス(「QR コードによるゲストの招待」「顔認証やスマホによる解錠」など)は ABEJA が開発・運用しています。当然、利用ユーザーが増えるごとに改善事項が見えてくるわけで、日々アップデートに励む毎日です。3

Bizflex を支えるサービスの裏側については、昨年の Advent Calendar で詳しく書いていますので、ぜひご一読ください(フロントエンド編/バックエンド編)。

昨年に引きつづき、今年も主にフロントエンドのコードを書いていたのですが、特に iOS 向けのネイティブコードを書くことが多かったです。この記事では、それらのコードを紹介しつつ、背景や苦労した点を書いてみたいと思います。

カメラのビデオフレームをキャプチャしたい

Bizflex では、受付に設置されている iPad に顔を映すことで、入居者はドアのロックを解錠できるようになっています(以下「顔認証機能」と呼びます)。この顔認証機能を実現するために、iPad で稼働しているアプリでは、フロントカメラに顔が映ったときに映像データを取得する必要がありました。

カメラで写真を撮影するのは簡単です。

Bizflex のアプリは ExpoReact Native で実装されているので、Camera コンポーネントを使えばさほど難しくはありません。4 しかし、開発中に、カメラによる写真撮影にはいくつかの課題があると判明しました。

  1. (日本版の iPad だと)シャッター音を消すことができない
  2. 若干処理が重い(撮影された側が気づくくらいのラグが生じる)

どちらも「機能を実現するだけであれば動くが、できればなんとかしたい問題」でした。特に 1 は受付端末を使う利用者に不快感を与える要因にもなりうるため、割と重要な問題です。

いくつか試行錯誤した結果、カメラのビデオフレームをキャプチャして画像化するのが良さそう、という結論に落ち着きました。ビデオフレームからの画像変換は Video Toolbox フレームワークが使えます。

// buffer は CMSampleBufferRef
CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(buffer);
CGImageRef videoFrameImage = NULL;

CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);

const OSStatus status = VTCreateCGImageFromCVPixelBuffer(pixelBuffer, NULL, &videoFrameImage);

CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);

if (status != errSecSuccess) {
  return [NSNull null];
}

// 画像化
UIImage *img = [UIImage imageWithCGImage:videoFrameImage];

昔懐かしい Objective-C のコードです。5

たったこれだけのコードですが、無事にカメラに映った顔をシャッター音なしで画像化できるようになりました。しかも、処理は高速で、問題点の 2 も同時に解決できたことになります。6

BLE のペリフェラルを実装したい

Bizflex では上記の顔認証機能と似たような機能として、iOS/Android アプリから近くのドアを解錠できる機能(以下「スマホ証機能」と呼びます)があります。

近くにある受付端末と通信することになるので、BLE (Bluetooth Low Energy) が使えそうです。また、スムーズなユーザー体験のために、アプリを起動してから解錠までの時間を短くしたい、という目標がありました。

高速化のためには、BLE による通信の回数をできるだけ減らすことが重要です。

検討した結果、以下のような仕組みになりました。iOS/Android アプリを BLE のセントラル (Central)、受付端末の iPad をペリフェラル (Peripheral) とします。

  1. iPad は事前に、自分の秘密鍵をバックエンドサーバーに登録する
  2. iPad は定期的に自分の存在をブロードキャスト(アドバタイズ)し、iOS/Android アプリが認識できるようにする
  3. iOS/Android アプリは見つかった iPad に接続する
  4. iPad は接続されたら、自分の秘密鍵で署名したトークン(有効期限つき)を送信する
  5. iOS/Android アプリは、受け取ったトークンを API でバックエンドサーバーに送信する
  6. バックエンドサーバーは API リクエストおよびトークンを検証した上で、ドアを解錠する 7

上記のステップのうち、BLE 通信が必要なのは 3, 4 のみであり、iOS/Android アプリからスキャンして接続するだけで完了します。8 実際に簡単なプロトタイプを実装して試してみたところ、満足できる速度で実現できそうなことがわかりました。

さて、あとはこれを Expo/React Native で実装するだけなのですが、ここで問題が発生します。React Native で BLE を実装するためのライブラリを探してみたところ、いくつか見つかったのですが、いずれもセントラル側のみの実装であったり、品質やメンテナンスに難がありそうなものばかりでした。

しかたがないので、Apple の Core Bluetooth フレームワークを使い、必要な部分だけを Native Module として独自実装しました。9

分割 QR コードの読み取り

あまり広くは知られていませんが、QR コードには連結機能があります。ひとつの QR コードに格納できる文字数には上限があるため、複数に分割して読み取ったあとで連結できるようになっています。10

QRコードの概要 (PDF) の「2-5. シンボルの連結機能」より抜粋

この連結機能によって分割された QR コードには、何番目の QR コードかを示すインデックス分割数が以下の形式で埋め込まれています。

+--------------------+-------------------+-------------------+
| mode (4bits = 0x3) | seq index (4bits) | seq total (4bits) |
+--------------------+-------------------+-------------------+

カメラで QR コードをスキャンするのは簡単です。

Bizflex のアプリは ExpoReact Native で実装されているので、BarCodeScanner コンポーネントを使えばさほど難しくはありません。

しかし、残念ながら、QR コードの連結機能には対応していないようでした。

Apple の AVFoundation フレームワークを直接使えば対応できそうな情報 11 は見つかりましたが、AVFoundation に対応したカメラコンポーネントを一から書くのは少々面倒です。

そこで、今回も VisionCamera のプラグインとして実装することにしました。バーコード (QR コード) の読み取りには、Zxing というライブラリが実装も枯れており、Objective-C へのポートもされていたので採用しました。こんな感じで、読み取ったメタデータからインデックスと分割数を取り出してアプリから取得できるようにしています。

また、連結機能で分割された QR コードを数多く(最大 16 個)読み取らないといけないので、一度にできるだけ多くの QR コードを読み取れるように色々試行錯誤していたのを覚えています。

どうして、すべて Objective-C になってしまったのか...

どうして Swift ではなく Objective-C での実装ばかりになったのかを説明して記事を終わりたいと思います。

単純に Swift よりも Objective-C の方が慣れている 12 というのもありますが、脚注 [^9] にも書いたように、React Native の新しい Turbo Native Module への対応が主な理由でした。Turbo Native Module の API が C++ で実装されていることから(当時は結局対応しなかったのですが)C/C++ と親和性の高い Objective-C で書いておくのがいいだろう、という判断でした。

「でも、やっぱり Swift で書きたい...😢」

そんなふうに思いながら悔し涙を流していたとき... Expo SDK 47 で Module API が正式リリースされました!

Expo Modules API 1.0: JSI & Fabric native modules with an idiomatic Swift & Kotlin API

🎉

今は Expo で実装している各プロジェクトを Expo SDK 47 に移行しているところです。転生したら、来年からは Swift/Kotlin でネイティブモジュールを実装していきたいと思います。

最後に

では最後に、お約束の宣伝です。

株式会社ABEJAでは共に働く仲間を募集しています! Bizflex はもちろん、新規事業や DX/AI に興味がある方は、ぜひこちらの採用ページからエントリーください。

careers.abejainc.com

また、来年 2023 年には Bizflex の新しいラインナップが増えます! 4月には「Bizflex 六本木」がローンチ予定なので、どうぞよろしくお願いします!


  1. ABEJA システム開発グループと取り組みのご紹介 - ABEJA Tech Blog
  2. それまではサテライトオフィスの位置づけでした。本社移転のお知らせ|株式会社ABEJA
  3. いわゆる「ドッグフーディング」というやつですね。
  4. 実際、プロトタイプ段階では写真撮影で実装していました。
  5. iOS のアプリ開発といえば Swift ですが、今回はのちほど説明する事情により Objective-C で書いています。
  6. 実際のアプリケーションでは、VisionCamera というカメラ・ ライブラリのプラグインとして実装しています。VisionCamera のプラグインは React Native Reanimated の Worklet として動作するため、メインの UI スレッドと並列して実行できます。余談ですが、撮影タイミングを知るための顔の検出も、VisionCamera の face detector プラグインを使っています。
  7. リモートサーバーからのドア解錠の仕組みについてはオフィスDXを支える技術(バックエンド編に詳しく書いてあります。
  8. 単純で検証もしやすくセキュアな方式なのですが、もちろん欠点もあります。ステップ 6 のバックエンドサーバーとの通信が iOS/Android アプリで行われるため、スマートフォンの通信環境によっては遅延の原因になる可能性が考えられます。通信を iPad から行うことで(iPad は館内ネットワークで接続できるので)通信速度は安定化しそうですが、どうしても複雑な仕組みになりそうで保留しています。
  9. React Native の新しい Turbo Native Module で実装することを検討したのですが、当時はまだ、React Native 0.68 に対応した Expo SDK 45 に移行するタイミングだったことと、そもそも Turbo Native Module の情報が少なすぎて一旦保留として Native Module で実装しました。我々のアプリに必要な最低限の機能しか実装していませんが、コードはこちらで公開しています。
  10. QRコードの概要 (PDF) の「2-5. シンボルの連結機能」参照
  11. 【Swift】QRコードの分割情報を取得する - Qiita
  12. Swift もある程度書いていますが、Objective-C は手動でリファレンスカウントしていた時代から書いているので手に馴染んでる感が違います。