🤝

TCAバージョン1.17.0のマイグレーション

に公開

TCA 1.17.0

2024/12/3にTCAのバージョン1.17.0がリリースされました。
https://github.com/pointfreeco/swift-composable-architecture/releases/tag/1.17.0
メイントピックはShared関連がライブラリとして切り出されたことで、非互換性の変更が加わったことです。以降このライブラリをSharingと呼びます。
https://github.com/pointfreeco/swift-sharing
TCAのとSharingのそれぞれでマイグレーションガイドが示されています。

TCAのマイグレーション

https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/migratingto1.17

TCAのマイグレーションガイドには以下のように書かれています。(翻訳)

当分の間、Sharingの後方互換性のあるバージョンに留まりたい場合は、1.0未満のバージョンに固定するためにライブラリに明示的な依存関係を追加することができます

TCAの1.17.0未満で組み込まれていたSharedの機能と、Sharingの1.0.0以上の機能には互換性がありません。そのためTCAの1.17.0未満から1.17.0以上へアップデートした際にSharingの非互換性によってビルドができなくなることを避けたい場合は、明示的にSharingの1.0.0未満を依存させる必要があります。

-.package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.16.0"),
+.package(url: "https://github.com/pointfreeco/swift-composable-architecture", exact: "1.17.0"),
+.package(url: "https://github.com/pointfreeco/swift-sharing", from: "0.1.0"),

Sharingのマイグレーション

https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/migratingto1.0

Sharingでは1.0.0へのマイグレーションガイドが公開されています。Sharingの1.0.0未満が(おおよそ)TCAに組み込まれていた時の内容と捉えて良いです。

変更のAPIの廃止

今までは一般の変数のようにセッターを利用できました。

@Shared(.inMemory("isOn")) var isOn = true
isOn = false

しかしこの変更方法はデータ競合によるクラッシュが起こる恐れがありました。
https://github.com/pointfreeco/swift-composable-architecture/discussions/3182

Sharingの1.0.0から通常のセッターは禁止されコンパイル時にエラーとなるようになりました。withLockがマイグレーション先となります。

$isOn.withLock { $0 = false }

SharedのPublisherに初期値が追加

今まではSharedのプロパティをCombineのPublisherで購読する場合、初期値はストリームに流れませんでした。

@Shared(.inMemory("isOn")) var isOn = true
$isOn.publisher
    .sink { print($0) }
    .store(in: &cancellables)
$isOn.withLock { $0 = false }
// false

Sharingの1.0.0からは初期値も流れるようになりました。

$isOn.publisher
    .sink { print($0) }
    .store(in: &cancellables)
// true
$isOn.withLock { $0 = false }
// false

1.0.0未満同様に初期値を含ませたくない場合はPublisherの初めの値をストリームに流さないようにするdropFirstを利用します。

$isOn.publisher
    .dropFirst()
    .sink { print($0) }
    .store(in: &cancellables)
$isOn.withLock { $0 = false }
// false

名前の変更

Sharingの1.0.0から型名やメソッド名にいくつか変更がありました。特に自作のSharedを作成することに関係する型名に変更が多くあります。詳しくは公式の対応表を参照してください。
https://swiftpackageindex.com/pointfreeco/swift-sharing/main/documentation/sharing/migratingto1.0#Other-renames

以下は自作のRemoteConfigのSharedのマイグレーション例です。
1.0.0未満

struct RemoteConfigReaderKey: PersistenceReaderKey {
    var id: some Hashable { "RemoteConfigReaderKey" }
    // RemoteConfigの変更をAsyncStreamで流すAPIを持つクライアント
    @Dependency(RemoteConfigClient.self) private var client
    
    func load(initialValue: RemoteConfig?) -> RemoteConfig? {
        client.value()
    }
    
    func subscribe(initialValue: RemoteConfig?, didSet: @escaping @Sendable (RemoteConfig?) -> Void) -> Shared<RemoteConfig>.Subscription {
        let task = Task {
            for await value in client.values() {
                didSet(value)
            }
        }
        return Shared<RemoteConfig>.Subscription { task.cancel() }
    }
}

extension PersistenceReaderKey where Self == PersistenceKeyDefault<RemoteConfigReaderKey> {
    static var remoteConfig: Self {
        let readerKey = RemoteConfigReaderKey()
        return Self(readerKey, readerKey.load(initialValue: nil)!)
    }
}

1.0.0以降

struct RemoteConfigReaderKey: SharedReaderKey {
    var id: some Hashable { "RemoteConfigReaderKey" }
    @Dependency(RemoteConfigClient.self) private var client
    
    func load(initialValue: RemoteConfig?) -> RemoteConfig? {
        client.value()
    }
    
    func subscribe(initialValue: RemoteConfig?, didSet: @escaping @Sendable (RemoteConfig?) -> Void) -> SharedSubscription {
        let task = Task {
            for await value in client.values() {
                didSet(value)
            }
        }
        return SharedSubscription { task.cancel() }
    }
}

extension SharedReaderKey where Self == RemoteConfigReaderKey.Default {
    static var remoteConfig: Self {
        let readerKey = RemoteConfigReaderKey()
        return Self[readerKey, default: readerKey.load(initialValue: nil)!]
    }
}

おわり

そろそろ2.0.0が待ち遠しいですね。
おわり〜〜〜〜〜〜〜

Discussion