STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

STORES レジにおけるSwift6移行対応

この記事は STORES Advent Calendar 2024 の16日目の記事です。

はじめに

こんにちは、STORES レジ でアプリ開発している @nekowen です。

STORES のモバイルプロダクトは STORES レジ 以外に「STORES ブランドアプリ」「STORES 決済」「STORES 予約」が存在しますが、今年の下期から各プロダクトで Swift6 に移行する取り組みを行なっています。

本記事では STORES レジ で取り組んでいる Swift6 移行対応についてかんたんにご紹介します。

実施計画と見積もり 🤔

レジアプリは SwiftPM を用いたマルチモジュール構成となっており、モジュール毎にSwift6への切り替えを行なっていく必要があります。
その場合、どのモジュールから着手するかを検討しなければなりません。

Swift 公式の Migration Guide、「Migration Strategy」によると、最も外側のモジュールから始めるべきと示されています。
レジアプリもこの方針に従って進めることにしました。

www.swift.org

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 というツールで依存関係を洗い出すことにしました。

github.com

これは Xcode のプロジェクトファイルおよび Package.swift を分析してモジュールの依存関係をビジュアライズしてくれるコマンドラインツールです。
こちらを使うことですぐにどのモジュールから手をつければ良いかわかり、またどのように進めていくか計画が立てやすくなったので非常におすすめです。

Mermaidにも対応してます

次に、モジュール毎の移行対応のボリューム感を調査しました。
これはモジュール1つずつ Swift6 モードに切り替え、出てきた Error・Warning 数をまとめた表です

厳密には修正することで別の Error・Warning が出てくることがあるので、この数値はあまり正確ではありません。
またこの数値を元に見積もりを出す場合、エラーの内容によってはコードを大きく変える必要のある箇所も出てくるためそのブレの考慮も必要です。

スケジュールは進行具合を見ながら適宜調整することは考えていたので、一旦これらの数値を使うことを許容し、モジュール毎の対応工数をざっくり割り出しスケジュールを仮決めしていきました。

移行作業🏃‍♀️

スケジュールを引いたのであとはひたすらに移行対応を進めていきました。
ここでは具体的にどのような対応をしたか2点紹介します。

existential any の対応

元となるProposalはこちらです

github.com

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() {}
}

これは HogeControlManagerSendable でないため、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 のみ継承していること

developer.apple.com

上記のサンプルコードの例ですと、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 への移行割合が少ない状況ではあるので、来年に向けて徐々に移行作業を進めていければと考えています。