freeeの開発情報ポータルサイト

freee 会計ソフト iOS のレシート撮影カメラをリニューアルしました

Hello, world.
会計ソフト iOS チームで開発をしている Kirk(カーク)です。
みなさまとのご縁で生きながら、コントラバスを弾くためにコードを書いています。

今回、恐らくユーザーからは念願であったであろう、レシート撮影で使用するカメラのリニューアル構想、設計、実装を担当したのでその内容を共有します。

リニューアルされたカメラ📱📸

百聞は一見にしかず、でございます。 こちらのデモ動画をご覧ください 💁‍♂️ < ミテネ

www.youtube.com

おわかりだろうか…このデモ動画内では、撮影者は手動でシャッターは押していないのです!そう、自動でレシートを認識して撮影する、自動撮影を新しく機能追加しました 😎
さらにカメラ画面自体も一新しました!

新カメラ 旧カメラ

SwiftUI、UIKit、AVFoundation、Vision Framework、Concurrency など、iOS が提供する機能を贅沢に使用しています。

ストーリー

🧞‍♂️ < 自動撮影機能を実装せよ

このプロジェクトは私が天啓を受けたことから始まりました。それを受けてまず既存のカメラをベースに日々のタスクの合間でプロトタイピングを開始しました。
ある程度形になった時点で Slack の miteyo チャンネル(エンジニア、デザイナーなどが「見てほしいこと、やってみたこと」を共有する場)へプロトタイプの内容を投稿すると、チーム内での反響がとても良く、「このままカメラリニューアルも行い、確定申告期までにリリースしよう」とプロダクトマネージャーと合意後、デザイナーも巻き込み正式にプロジェクトとしての開発がスタートしました。

機能

自動撮影

先述の通り、今回新機能として自動撮影を導入し、例えば、左手でスマホをレシートへかざしながら、右手で次々とレシートを差し替えて撮影したりすることが可能になりました。
また認識中の書類の位置をリアルタイムで計算し、ズレ(移動距離)が大きい時は書類認識の処理を中断することにより、手ブレを防止しレシート取り直しの可能性を減らしています。 今回のカメラリニューアルでは書類、レシート認識を Apple が提供する Vision Framework の VNDetectDocumentSegmentationRequest を使用しています。これによって以前よりも優れた精度で書類やレシートを検知することが可能になっています。

developer.apple.com

自動撮影で表示しているローディング、円が回転するアニメーションには、とてもこだわりました。
回転するスピード、シャッターを切る直前の気持ちの良い動き。デザイナーとも意見を交わしながらプロトタイプを基に詰めていきました。
アニメーションはパフォーマンスを意識して CoreAnimation のCABasicAnimationCAKeyframeAnimation を採用しています。

developer.apple.com

自動補正

書類、レシート上に表示されている薄い青のエリアを、撮影した画像全体から切り取る機能です。
旧カメラとは違い、先述の VNDetectDocumentSegmentationRequest の利用により精度がとても向上しています。適切に切り取ることで撮影データ送信時の所要時間や、レシート内容を読み取る OCR 処理時間の短縮に貢献しています。
自動補正の検知イベントは、旧カメラでは OS からの画像出力全てに反応をしていたので、少し忙しい印象になっていました。そこを今回のリフォームでは Throttling を行って 10 回 / 1 秒 の検知イベントへ抑えています。

developer.apple.com

実装で注意したこと

何が最低限必要か、を意識する

ソフトウェア開発では、アニメーションや使い心地などを議論したり突き詰めたりすると、プロダクトをリリースするまでに必要以上に時間を要してしまうことがあります。「ユーザーに届ける」ことを第一に考えて、チームメンバー、デザイナー、プロダクトマネージャーと会話しながら開発を行いました。地味なことですが、とても大切にしています。

プログラム、コードの責務を分ける

このカメラ画面には、とても、とても多くのプログラム上の機能が存在しています。自動撮影の書類検知のタイマー、シャッターボタンが押せるかどうか、アップロード可能なタイミングの調整、iOS のカメラの処理をコントロール、などなど。それらを AnimationManager、UI Manager、AutoCaptureManager などに分けて、それぞれの処理、責務の所在を可能な限り分けるよう努めました。

Swift Concurrency でコードを読みやすく、理解しやすくする

自動撮影を追加したことにより、例えば撮影したレシートのアップロード処理中にカメラがレシートを捉えていると、使用者が意図しないタイミングで自動撮影が発生してしまう恐れがありました。これを防ぐ為には上手く非同期処理を行う必要がありますが、Swift の escaping callback で非同期処理を書くと、それはそれは複雑になり、流れを追いにくくなります…。
そこで Swift Concurrency を用いることで、例えば…

// アップロードボタンが押されたとき
func onTapUploadButton() {
    Task {
        // カメラを止める
        stopAutoCaptureCamera()
        // レシートをアップロードする
        let uploadedReceipts: [Receipt] = try await uploadReceipts(receipts: [Receipt])
        // 撮影したレシートから取引登録を行うか、アラートで確認
        let selected: Bool = await showActionAlert()
        if selected {
            // 取引登録登録画面を表示
            showCreateDeal(with: uploadedReceipts)
            return
        }
        ...
        // カメラを再開
        startAutoCaptureCamera()
    }
}

このように複雑な非同期処理を、API などの通信だけでなく View イベント等でも可読性高く書くように心がけました。これは本当にありがたい言語機能です…

developer.apple.com

おわりに

天啓を受けて…など冗談を言いましたが、本音では「レシート撮影体験の向上はユーザーに取って必要なことだ」と、強い気持ちを持って開発を進めました。今後もユーザーの皆様の為に有意義なリリースをしていきたいです。ありがとうございました 👋