この記事は STORES Advent Calendar 2024 の16日目の記事です。
はじめに
こんにちは、STORES レジ でアプリ開発している @nekowen です。
STORES のモバイルプロダクトは STORES レジ 以外に「STORES ブランドアプリ」「STORES 決済」「STORES 予約」が存在しますが、今年の下期から各プロダクトで Swift6 に移行する取り組みを行なっています。
本記事では STORES レジ で取り組んでいる Swift6 移行対応についてかんたんにご紹介します。
実施計画と見積もり 🤔
レジアプリは SwiftPM を用いたマルチモジュール構成となっており、モジュール毎にSwift6への切り替えを行なっていく必要があります。
その場合、どのモジュールから着手するかを検討しなければなりません。
Swift 公式の Migration Guide、「Migration Strategy」によると、最も外側のモジュールから始めるべきと示されています。
レジアプリもこの方針に従って進めることにしました。
Begin from the Outside It can be easier to start with the outer-most root module in a project. This, by definition, is not a dependency of any other module. Changes here can only have local effects, making it possible to keep work contained.
レジアプリはモジュールの依存関係がやや複雑になっていたのもあり、どのモジュールが依存関係にあるかがイマイチ不透明でした。
これでは外側のモジュールから進めるにしても計画を立てづらかったため、まずは dependency-graph
というツールで依存関係を洗い出すことにしました。
これは Xcode のプロジェクトファイルおよび Package.swift
を分析してモジュールの依存関係をビジュアライズしてくれるコマンドラインツールです。
こちらを使うことですぐにどのモジュールから手をつければ良いかわかり、またどのように進めていくか計画が立てやすくなったので非常におすすめです。
次に、モジュール毎の移行対応のボリューム感を調査しました。
これはモジュール1つずつ Swift6 モードに切り替え、出てきた Error・Warning 数をまとめた表です
厳密には修正することで別の Error・Warning が出てくることがあるので、この数値はあまり正確ではありません。
またこの数値を元に見積もりを出す場合、エラーの内容によってはコードを大きく変える必要のある箇所も出てくるためそのブレの考慮も必要です。
スケジュールは進行具合を見ながら適宜調整することは考えていたので、一旦これらの数値を使うことを許容し、モジュール毎の対応工数をざっくり割り出しスケジュールを仮決めしていきました。
移行作業🏃♀️
スケジュールを引いたのであとはひたすらに移行対応を進めていきました。
ここでは具体的にどのような対応をしたか2点紹介します。
existential any
の対応
元となるProposalはこちらです
Swift6からデフォルト有効化される機能で、存在型に any
をつけていないとビルドエラーになります。
こちらはひたすら any
or some
をつけて対応しましたが、プロダクトの規模によっては変更量がかなり大きくなるはずなので最初にやっておくと良いでしょう。
Sendable
の対応
例えば以下のようなマネージャクラスの実装は、Swift6 ではエラーとなります。
class HogeControlManager { static let shared = HogeControlManager() <-- Static property 'shared' is not concurrency-safe because non-'Sendable' type 'HogeControlManager' may have shared mutable... func run() {} }
これは HogeControlManager
が Sendable
でないため、shared
プロパティの並行安全が担保できないことを示しています。
レジアプリでもこのタイプのエラーが大量に出てきたので、以下の3パターンで対応しました。
1. class / struct を Sendable
プロトコルに準拠させる
Sendable
はコンテキスト間で値を共有できるスレッドセーフなプロトコルです。
Apple の公式ドキュメントによると、Sendable
に準拠させられる条件は以下のように記載されています
struct
Sendable
が宣言されていること- メンバおよび Associated Values がすべて
Sendable
であること
ただし以下のパターンは暗黙的に Sendable
に対応するため宣言が不要
@frozen
が付与された struct であること- public ではなく、
@usableFromInline
がマークされていない struct であること
class
Sendable
が宣言されていることfinal
がマークされていること- immutable かつ
Sendable
に準拠したストアドプロパティのみ持つこと - 親クラスを継承しないこと、または
NSObject
のみ継承していること
上記のサンプルコードの例ですと、final
をマークし、Sendable
を継承するだけでエラーが解消できます。
final class HogeControlManager: Sendable { static let shared = HogeControlManager() <-- OK! func run() {} }
2. @unchecked Sendable
を付与する
class が NSObject
以外を継承しているなど、何らかの理由で Sendable
に対応できない場合もあると思います。
その場合、class に排他制御を入れていてスレッドセーフを保証できる状況だったり、スレッドセーフだとわかる場合は @unchecked Sendable
を用いることで Sendable
であるとみなすことができます。
class HogeControlManager: @unchecked Sendable { static let shared = HogeControlManager() func run() {} }
3. @MainActor
または actor
へ切り替え
上記2つでの対応が難しい場合は @MainActor
を付与するか、あるいは classではなく actor
に切り替える方法を採用しました。
ただ現状レジアプリの対応では大体 @MainActor
の付与で十分なケースが多く、actor
まで使うケースは少なかったです。
@MainActor class HogeControlManager { static let shared = HogeControlManager() func run() {} } actor HogeControlManager { static let shared = HogeControlManager() func run() {} }
最後に
かんたんな説明でしたが、 STORES レジ における Swift6 移行対応を紹介しました。事例として参考になれば幸いです。
モジュール単位で見るとまだ Swift6 への移行割合が少ない状況ではあるので、来年に向けて徐々に移行作業を進めていければと考えています。