iOS開発の福山です。 現在Mirrativ iOSではSwift 6への移行を段階的に行なっています。その中でSwift 6に対応していないサードパーティライブラリに関連する問題にぶつかったため、その回避策を紹介します。
問題
open class Some3rdPartyClass { // サードパーティのライブラリなので変更が容易ではない open func doSomething(completion: @escaping (Bool) -> Void) { } } // ------------------------- final class SomeSubclass: Some3rdPartyClass { override func doSomething(completion: @escaping (Bool) -> Void) { // error: // Passing non-sendable parameter 'completion' to function expecting a @Sendable closure doSomeAction(completion: completion) doSomeAction { result in // error: // Capture of 'completion' with non-sendable // type '(Bool) -> Void' in a `@Sendable` closure completion(result) } } // Sendableなclosureを必要とする何らかの処理 func doSomeAction(completion: @escaping @Sendable (Bool) -> Void) { ... } }
ここではSome3rdPartyClass
はサードパーティライブラリから提供されているクラスであり、コードの改変が容易ではありません。そのdoSomething
メソッドのcompletion
引数に渡されるクロージャは、Sendable(異なるConcurrency domain間で安全に受け渡しできるプロトコル)準拠ではなく、それをoverrideしている箇所でSendableにすることもできません。しかし、doSomeAction
メソッドではSendableなクロージャを必要とするため、エラーが発生します。
非SendableなクロージャをSendableなクロージャへ引数として渡す際に以下のようなエラーとなります。
Passing non-sendable parameter 'completion' to function expecting a @Sendable closure
また、非SendableなクロージャをSendableなクロージャの中で使用すると以下のようなエラーとなります
Capture of 'completion' with non-sendable type '(Bool) -> Void' in a `@Sendable` closure
解決策
open class Some3rdPartyClass { // サードパーティのライブラリなので変更が容易ではない open func doSomething(completion: @escaping (Bool) -> Void) { } } // ------------------------- /// サードパーティライブラリからのsubclassやprotocol準拠により /// Sendableなclosureが作れない場合などに強制的にコンパイラ検証を回避するラッパー /// 並行処理の安全性を保証する責任は開発者に委ねられる点に注意 private struct UncheckedClosure<T>: @unchecked Sendable { typealias Closure = (T) -> Void let closure: Closure? init(_ closure: Closure?) { self.closure = closure } } final class SomeSubclass: Some3rdPartyClass { override func doSomething(completion: @escaping (Bool) -> Void) { let uncheckedClosure = UncheckedClosure(completion) doSomeAction { result in uncheckedClosure.closure?(result) } } // Sendableなclosureを必要とする何らかの処理 func doSomeAction(completion: @escaping @Sendable (Bool) -> Void) { ... } }
この方法は、サードパーティの制約に対応するために非Sendableなクロージャをラップし強制的にSendableとして扱う方法です。@unchecked Sendable
のstruct UncheckedClosure
でラップすることによりコンパイラによるConcurrency検証をスキップできます。並行処理の安全性を保証する責任は開発者に委ねられる点に注意が必要です。
おわりに
サードパーティライブラリがSwift 6対応するまでの一時的な措置の紹介でした。より良い方法や知見がありましたらXまでご連絡いただけると幸いです。
Mirrativ iOSでのSwift 6移行はSwift Package側は完了していますが本体側はまだ道半ばです。コンパイラによるデータ競合チェックから得られる恩恵は大きいと考えているため、チームのメンバーと知見を共有しつつ着実に移行を進めていきたいです。