とあるプロジェクトのリファクタリング作業をしていて、@retroactiveをつけてねってワーニングが出ている部分に気づいた。

そもそも@retroactiveって何のことか知らなかったのでここで軽く備忘録としておきます。
Contents 非表示
retroactiveの意味
そもそもの言葉の意味;
retroactive = 遡及的 (さかのぼって効力を持つ)
つまり、何かしらのルールなりがあって、遡って今にもその効力があるという感じの意味合いらしい。
今回のSwiftで扱われる意味合いとしては、
自身が変更できない外部ライブラリ(Firebase、Realm、Amplify、GraphQLなどのSDK)から提供されるデータモデルをプロジェクトで適用したいタイプやデリゲートに適合させる際に不可欠なもの
という感じになりそうです。
例
import SwiftUI
import ExternalUserSDK // サードパーティーSDKを想定
// MARK: - Retroactive経由のSwiftUI統合 (Swift 6以降では宣言が必須らしい?)
extension ExternalUser: @retroactive Identifiable {
public var id: String {
return self.uuidString
}
}
extension ExternalUser: @retroactive Hashable {
public static func == (lhs: ExternalUser, rhs: ExternalUser) -> Bool {
return lhs.uuidString == rhs.uuidString
}
public func hash(into hasher: inout Hasher) {
hasher.combine(self.uuidString)
}
}
// MARK: - SwiftUI View
struct UserListView: View {
let users: [ExternalUser]
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
List(users) { user in // Requires Identifiable
NavigationLink(user.displayName, value: user)
}
.navigationDestination(for: ExternalUser.self) { user in
Text("Detail of \(user.displayName)")
}
}
}
}内製コード(自前の構造体)で完結させる場合は問題ないけど、大規模開発においてマルチプラットフォーム向けパッケージ(Kotlin Multiplatform Mobileとか)や既存のパッケージ(Carthage/SPM経由のバイナリなど)の型をそのままSwiftUIのレイヤーで流用する場合、@retroactive による遡及的適合が手軽で実用的な解決策となりそう。
Swift 6における「危険の明示」
Swift 6のデータ隔離(Data Isolation)とモジュール安全性の強化に伴い、この @retroactive を付与せずに外部型を外部プロトコルに適合させようとすると、コンパイルエラー(または厳格な警告)が出力されるそうです。
これは、SDK側が将来のアプデで同名のプロトコルに自己準拠した際、アプリ側とSDK側の双方の実装が衝突し、アプリが起動時クラッシュを起こすリスクを警告しているからという背景だそうです。
ベストプラクティスとしてのアーキテクチャ上の判断
@retroactive は便利だけど、多用しすぎると外部SDKの仕様変更による潜在的な破壊的変更に対して脆くなる。
なので、手軽にプロトタイプや小規模アプリを作るなら @retroactive。中〜大規模なプロジェクトでは、ドメインモデルとUI表示用モデル(View型)を明確に分離し、Adapterパターン等でマッピングするという設計方針の使い分けがベターになりそうですね。
