@Sendableなクロージャについて

# はじめに
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なクロージャでキャプチャして参照してはいけないというものです。

# コードの内容
具体的なコードとしては以下です。

```swift
@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`の記載があります。
```swift
    @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とは？
公式のドキュメントでは以下のように説明されています。

>データ競合のリスクを招かずに、任意の同時コンテキスト間で値を共有できるスレッドセーフな型。

https://developer.apple.com/documentation/swift/sendable

ドキュメントにも記載のある通り、データ競合が起こらないことを保証する型というのがSendable型になります。

### データ競合(data race)とは？
複数のスレッドから共有した可変の状態(classの変数など)に少なくとも1つ以上の書き込みが含まれるアクセスを同時に行われることを指します。
データ競合が起きるとさまざまな意図しない挙動が起きうることになります。
例えばアプリがクラッシュするなどです。

類似した概念に競合状態(data condition)がありますが別物です。
下記記事でどう違うのか大変わかりやすく説明されていました。

https://qiita.com/yohhoy/items/00c6911aa045ef5729c6

`Sendable`について詳細に説明しようとすると、膨大な長さの記事になりそうなのでここでは、`Sendable`に適合した型ではデータ競合が発生しないということだけ抑えておけば良いと思います。
(`Sendable`についての詳細はまた別記事書きたいです)

### 上記を踏まえて`@Sendable`なクロージャがどういうものか見る
先ほど`Sendable`な型ではデータ競合が起こらないと説明しました。
よって、`@Sendable`なクロージャでもデータ競合が起きないということが保証されています。
ではデータ競合が起きうるクロージャというのはどういうクロージャでしょうか？

例えば以下のような`completion`クロージャはデータ競合が起きそうです。
クロージャ内でキャプチャしている`counter`が可変状態を持っており、複数スレッドで`completion`が実行されると、同時に`counter`が書き込まれる可能性があります。

```swift
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`の要件を満たせそうです。

修正したコードが以下になります。

```swift
@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についての理解がかなり深まったように思います。
上記を調べる上で以下動画が大変助けになりましたので、参考資料として貼っておきます。

https://www.youtube.com/watch?v=jJgEtjx8KHY