はじめに
swift concurrencyの対応をしている中で、以下のようなワーニングが出たので、その対応方針について検討していたら@Senbable
なクロージャってなんだ?となったので調べてみました。
Main actor-isolated property 'completion' can not be referenced from a Sendable closure; this is an error in the Swift 6 language mode
まず上記のワーニングで怒られているのは、Sendableでないプロパティ'completion'がSendableなクロージャでキャプチャして参照してはいけないというものです。
コードの内容
具体的なコードとしては以下です。
@MainActor
class SingleSelectImagePickerWrapper: AssetPickable {
// MARK: - private property
private var completion: (Result<UIImage, Error>) -> Void = { _ in }
private var isSelected = false
// MARK: - public method
func showPicker(_ target: UIViewController, selectionLimit: Int = 1,
assetType: AssetType) async throws -> UIImage {
let picker =
PHPickerViewController(configuration: createConfiguration(selectionLimit: selectionLimit,
assetType: assetType))
picker.delegate = self
target.present(picker, animated: true)
return try await withCheckedThrowingContinuation { [picker, weak self] continuation in
guard let self else {
print("Not found ImagePickerWrapper.")
return
}
completion = { result in
print("Finish Picker Image.")
switch result {
case let .success(image):
continuation.resume(returning: image)
case let .failure(error):
continuation.resume(throwing: error)
}
DispatchQueue.main.async {
// 結果を受けたらPickerは戻す
picker.dismiss(animated: true)
}
}
}
}
}
// MARK: - PHPickerViewControllerDelegate extension
extension SingleSelectImagePickerWrapper: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
// すでに選択済みの場合は、選択イベントを無視する
if isSelected { return }
isSelected = true
guard let itemProvider = results.first?.itemProvider else {
completion(.failure(AssertPickError.notFoundItemError))
return
}
guard itemProvider.canLoadObject(ofClass: UIImage.self) else {
completion(.failure(AssertPickError.failLoadItem))
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [completion] image, error in
if let error {
completion(.failure(error)) // ワーニング出力箇所
return
}
guard let image = image as? UIImage else {
completion(.failure(AssertPickError.notFoundItemError)) // ワーニング出力箇所
return
}
completion(.success(image)) // ワーニング出力箇所
}
}
}
上記コードは、アルバムから画像を選択できるピッカーを表示し、選択された画像をUIImageとして取得するということを行なっています。
その中のUIImageとして取得する部分にあたるloadObject
のクロージャでキャプチャしたcompletion
を参照したところでワーニングが表示されます。
loadObject
の定義を見ると、クロージャの型定義に@Sendable
の記載があります。
@available(iOS 11.0, *)
open func loadObject(ofClass aClass: any NSItemProviderReading.Type, completionHandler: @escaping @Sendable ((any NSItemProviderReading)?, (any Error)?) -> Void) -> Progress
@Sendable
なクロージャについて考える
Sendable
はプロトコルであり、classやstructなどに適合させてSendable
な型を定義していましたが、クロージャの型をプロトコルに準拠させることはできないので、@Sendable
という書き方でクロージャをSendable
に適合させる方法を提供してくれているみたいです。
そもそもSendableとは?
公式のドキュメントでは以下のように説明されています。
データ競合のリスクを招かずに、任意の同時コンテキスト間で値を共有できるスレッドセーフな型。
ドキュメントにも記載のある通り、データ競合が起こらないことを保証する型というのがSendable型になります。
データ競合(data race)とは?
複数のスレッドから共有した可変の状態(classの変数など)に少なくとも1つ以上の書き込みが含まれるアクセスを同時に行われることを指します。
データ競合が起きるとさまざまな意図しない挙動が起きうることになります。
例えばアプリがクラッシュするなどです。
類似した概念に競合状態(data condition)がありますが別物です。
下記記事でどう違うのか大変わかりやすく説明されていました。
Sendable
について詳細に説明しようとすると、膨大な長さの記事になりそうなのでここでは、Sendable
に適合した型ではデータ競合が発生しないということだけ抑えておけば良いと思います。
(Sendable
についての詳細はまた別記事書きたいです)
上記を踏まえて@Sendable
なクロージャがどういうものか見る
先ほどSendable
な型ではデータ競合が起こらないと説明しました。
よって、@Sendable
なクロージャでもデータ競合が起きないということが保証されています。
ではデータ競合が起きうるクロージャというのはどういうクロージャでしょうか?
例えば以下のようなcompletion
クロージャはデータ競合が起きそうです。
クロージャ内でキャプチャしているcounter
が可変状態を持っており、複数スレッドでcompletion
が実行されると、同時にcounter
が書き込まれる可能性があります。
class Counter {
var value = 0
}
var counter = Counter()
var completion: () -> Void = {
counter.value += 1
}
DispatchQueue.global.async {
completion()
}
DispatchQueue.global.async {
completion()
}
可変状態を持つクラスではなく定数のみのclassやstructなどSendable
なインスタンスをキャプチャする分にはデータ競合は起きません。
ワーニングの対応方法
ここまでわかると最初に記載したワーニングの対応方法が見えてきました。
キャプチャしているcompletion
プロパティがSendable
つまりデータ競合を起こさないことを保証していれば、loadObject
のクロージャにある@Sendable
の要件を満たせそうです。
修正したコードが以下になります。
@MainActor
class SingleSelectImagePickerWrapper: AssetPickable {
// MARK: - private property
private var completion: @Sendable (Result<UIImage, Error>) -> Void = { _ in } // Sendableに適合させる
private var isSelected = false
// MARK: - public method
func showPicker(_ target: UIViewController, selectionLimit: Int = 1,
assetType: AssetType) async throws -> UIImage {
let picker =
PHPickerViewController(configuration: createConfiguration(selectionLimit: selectionLimit,
assetType: assetType))
picker.delegate = self
target.present(picker, animated: true)
return try await withCheckedThrowingContinuation { [picker, weak self] continuation in
guard let self else {
print("Not found ImagePickerWrapper.")
return
}
completion = { result in
print("Finish Picker Image.")
switch result {
case let .success(image):
continuation.resume(returning: image)
case let .failure(error):
continuation.resume(throwing: error)
}
DispatchQueue.main.async {
// 結果を受けたらPickerは戻す
picker.dismiss(animated: true)
}
}
}
}
}
// MARK: - PHPickerViewControllerDelegate extension
extension SingleSelectImagePickerWrapper: PHPickerViewControllerDelegate {
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
// すでに選択済みの場合は、選択イベントを無視する
if isSelected { return }
isSelected = true
guard let itemProvider = results.first?.itemProvider else {
completion(.failure(AssertPickError.notFoundItemError))
return
}
guard itemProvider.canLoadObject(ofClass: UIImage.self) else {
completion(.failure(AssertPickError.failLoadItem))
return
}
itemProvider.loadObject(ofClass: UIImage.self) { [completion] image, error in
if let error {
completion(.failure(error))
return
}
guard let image = image as? UIImage else {
completion(.failure(AssertPickError.notFoundItemError))
return
}
completion(.success(image))
}
}
}
これで最初のワーニングは表示されなくなりました。
おわり
いかがだったでしょうか。
swift concurrencyのワーニング対応でswift concurrencyについての理解がかなり深まったように思います。
上記を調べる上で以下動画が大変助けになりましたので、参考資料として貼っておきます。