TCAバージョン1.17.0のマイグレーション
TCA 1.17.0
2024/12/3にTCAのバージョン1.17.0がリリースされました。
メイントピックはShared関連がライブラリとして切り出されたことで、非互換性の変更が加わったことです。以降このライブラリをSharingと呼びます。
TCAのとSharingのそれぞれでマイグレーションガイドが示されています。
TCAのマイグレーション
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のマイグレーション
Sharingでは1.0.0へのマイグレーションガイドが公開されています。Sharingの1.0.0未満が(おおよそ)TCAに組み込まれていた時の内容と捉えて良いです。
変更のAPIの廃止
今までは一般の変数のようにセッターを利用できました。
@Shared(.inMemory("isOn")) var isOn = true
isOn = false
しかしこの変更方法はデータ競合によるクラッシュが起こる恐れがありました。
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を作成することに関係する型名に変更が多くあります。詳しくは公式の対応表を参照してください。
以下は自作の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