Mirrativ Tech Blog

株式会社ミラティブの開発者(バックエンド,iOS,Android,Unity,機械学習,インフラ, etc.)によるブログです

Apple のサンプルプロジェクトから Swift Concurrency 移行のエッセンスを学ぶ

こんにちは、クライアントエンジニアのちぎら(@_naru_jpn)です。Apple が Swift Concurrency への移行をサポートするためのサンプルプロジェクト Updating an App to Use Swift Concurrency を公開しているのをご存知でしょうか。

このプロジェクトには、DispatchQueue を用いて記述されたアプリのコードと、それを Actor などの Swift Concurrency を使用したコードに書き換えたアプリのコードの 2 つが同梱されています。運用しているプロダクトのコードを書き換えるのは規模も大きく大変ですが、小さいプロジェクトから書き換えの課題感を掴んだり、ウォーミングアップとして Apple のサンプルプロジェクトから学べることがあるだろうという事から、このプロジェクトを題材にして社内勉強会を開催しました。

勉強会の概要

勉強会では、Apple のサンプルプロジェクトの全体像と書き換えの方針を掴むために ちぎら(@_naru_jpn)が スライド を作成して説明をし、より細かい箇所の説明やコードの書き換え作業は いっちー(@0IcchI)がスライドやコード編集画面の共有をしながら進めました。*1

この記事では、勉強会で使用したアプリの説明のためのスライドを使って、サンプルプロジェクトの全体像や狙いみたいなものの考察をお伝えできればと思います。コードの細かい書き換え部分には触れませんが、どういう部分が Actor に分離されるのか、などの考え方が共有できますと幸いです。

Updating an App to Use Swift Concurrency 解説

Updating an App to Use Swift Concurrency は、「Coffee Tracker」という Apple Watch 上のサンプルアプリケーションを題材としています。UI は SwiftUI で実装されていて、アプリの主な機能は「摂取したカフェイン量を記録して HealthKit と同期する」です。

アプリのインターフェース

画面の一覧

アプリは摂取したカフェイン量を確認する画面と、摂取した飲み物を選択する画面から構成されています。その他、コンプリケーションも用意されていますが、今回の移行においてはおまけと捉えてよいでしょう。

ユーザーの操作フロー

カフェインを摂取した際には、+ ボタンを押して飲み物一覧画面を表示した後、摂取した飲み物を選択してカフェインを記録します。ここで記録したカフェインの摂取はアプリ内に保存されると同時に HealthKit にも保存されます。

アプリの実装(Swift Concurrency 移行前と移行の構想)

従来通り DispatchQueue を使用して書かれた Swift Concurrency 移行前のプロジェクトの実装を調べてみましょう。

主要な登場人物

アプリの実装における主な登場人物をざっくりと分類してみました。

Drink と DrinkType

Drink は摂取したカフェインの量と日付、DrinkType は一覧画面に表示されている飲み物に対応する enum です。

アプリの主要な画面

CoffeeTrackerView は摂取したカフェインの量や何杯の飲み物を飲んだかを確認する画面、DrinkListView は飲み物の一覧を表示する画面です。

画面へのデータの反映とユーザーのアクション

各 View 上には EnvironmentObject として ObservableObject である coffeeData が存在します。CoffeeTrackerView 上ではカフェイン量や杯数の値が画面上に反映され、DrinkListView では飲み物が選択されると coffeeData.addDrink(...) が実行され、選択した飲み物に対応するカフェイン量が記録されます。

CoffeeData の主な内容

移行前の CoffeeData には主に2つの役割があり、一つはビューに提供する情報の元となる @Published な currentDrinks の保持、もう一つは currentDrinks の端末への保存と読み込みです。前者はメインスレッド上で行う必要があり、後者はバックグラウンドスレッドで実行されます。

HealthKitController の主な内容

HealthKitController は HealthKit からのデータの読み込みと保存、HealthKit から同期したデータを反映した結果を coffeeData.currentDrinks に格納するなどをしています。

公式のドキュメントによると HealthKit 上のデータへのアクセスはバックグラウンドスレッド上で行われます*2(つまり、コールバックのクロージャがバックグラウンドスレッドで実行されます)。一方 coffeeData.currentDrinks には @Published が付いていたのでメインスレッドから操作をする必要があります。

Swift Concurrency 移行前のアプリケーションの全体像

ここまでの説明をまとめると上のような絵を書くことができます。メインスレッドから操作する必要のある currentDrinks がオブジェクトをまたがって操作されていて、データの永続化や HealthKit からのコールバックがバックグラウンドスレッドから実行されています。これを元にしてどこを Actor で分離するかなど Swift Concurrency 移行の方針を決めていきます。

Swift Concurrency 移行の方針

上の構図を元にして、整理が必要な箇所を考えていきます。

移行前のプロジェクトで整理が必要な箇所

メインスレッドとバックグラウンドスレッドの処理が組み合わさっていたり、オブジェクトを跨いでプロパティをいじっていたりなどしている箇所は整理が必要です。

整理が必要な2ヶ所

Actor への置き換えなどを考える上で整理が必要な箇所は、以下の2つです。

  1. CoffeeData クラス内でメインスレッド・バックグラウンドスレッド上で行われる処理が混在している
    • メインスレッド上で currentDrinks の管理をしている
    • バックグラウンドスレッド上でデータをディクス上に保存/読み込みをしている
  2. HealthKitController クラス内で HealthKit からのコールバック処理を経由して CoffeeData クラスのプロパティを操作している。
    • CoffeeData クラスのプロパティ currentDrinks はメインスレッドから操作する必要がある
    • HealthKit のコールバック処理はバックグラウンド上で行われる

完成図

上記の観点を整理すると、CoffeeData クラスは、メインスレッドとバックグラウンドスレッド上の処理をそれぞれ MainActor と Actor として分離することができます。currentDrinks プロパティは MainActor となった CoffeeData クラスが保持するようになり、Actor となった HealthKitController クラスからは、必要な情報を添えて CoffeeData クラスにコミュニケーションをするという構造になっています。

アプリの実装(Swift Concurrency 移行後)

整理された後の CoffeeData, CoffeeDataStore, HealthKitController がどのような構成になったのかを見てみましょう。

currentDrinks を保持する MainActor である CoffeeData クラス

CoffeeData は MainActor となり、currentDrinks を保持するようになりました。移行前は currentDrinks に @Published が付いていてメインスレッド上から操作しているかどうかを気にする必要がありましたが、管理をするオブジェクトと操作される環境がより明示的になりました。

ディスクへの書き込み/読み込みを担当する CoffeeDataStore

元々 CoffeeData クラスの内部で行われていたディスクへの書き込み/読み込み処理は、Actor である CoffeeDataStore に切り出されました。CoffeeDataStoreCoffeeData が private なプロパティとして保持していて、バックグラウンドで処理を行いたいという意図も分かりやすくなりました。

HealthKit とやりとりをする Actor である HealthKitController

HealthKitController では currentDrinks へ直接アクセスすることはなくなり、CoffeeData に必要な情報を渡して処理をお願いする形に処理が変わっています。HealthKit のAPI も Swift Concurrency でおしゃれになっていました。

全体像の振り返り

最後に振り返りの一枚。

まとめ

出題の意図を探る受験生のような気持ちで Apple のサンプルプロジェクトの意図を探ってみました。バックグラウンド処理や HealthKit との連携を考慮して移行を考える必要があり、ウォーミングアップとしては優れたサンプルであると感じました。Swift Concurrency 移行は、局所的な書き換えルールはなんとなく分かっていても、いざ手をつけ始めると連鎖的にエラーが発生する危険性のある作業です。そういった際に、一歩引いて全体の役割を整理して考えることは、移行のゴールを明確にし、将来的なアプリの保守のしやすさにも繋がっていくのではないかと思います。

Mirrativ iOS アプリでは直近で Swift Concurrency 移行を予定している訳ではありませんが、色々な妄想をチームでできる環境を作っていくことは無駄にはならないと思いますし、いつか何かが役に立つかも知れません。役に立つ時が来るまで、カフェインの過剰摂取は控えて健康に精進していきたいものですね。

We are hiring!

ミラティブでは一緒にアプリを作ってくれる iOS エンジニアを募集しています!
少しでも興味を持っていただいた方はお話を聞いていただくだけでも結構ですので、気軽にご連絡ください。 www.mirrativ.co.jp

エンジニア向け会社紹介資料

speakerdeck.com