unowned/weak selfしているクロージャでTaskをつくると暗黙的キャプチャしない？

Swift Concurrencyは `Task` が作られたときや `await` したタイミングで暗黙的に `self` をキャプチャするようになっています。
このおかげで例えば非同期処理中に画面を閉じたとしても、先にリソースが解放されてしまうような事態が起きないようになっています。

https://www.swiftbysundell.com/articles/memory-management-when-using-async-await/

しかし `[unowned self]` や `[weak self]` でキャプチャしているクロージャ内では、この暗黙的キャプチャがされないケースがありました。

* Xcode 13.4.1 (Swift 5.6)
* Xcode 14.0.1 (Swift 5.7)

でこの現象が確認できましたが、今後のバージョンでは変わるかもしれません。

## 問題を再現するコード

モーダル画面が表示されてから約3秒後、アラートが表示されるコードを用意します。

<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25727/3ad08836-2091-83ec-c4f2-6697f9298f9a.gif">

```ModalViewController.swift
final class ModalViewController: UIViewController {
    private let viewModel = ModalViewModel()
    private var cancellables = Set<AnyCancellable>()

    deinit {
        print("deinit")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        bind()
        Task {
            // echo()は3秒後、viewModel.messageにエコーバックする
            await viewModel.echo(message: "ALERT!!")
        }
    }

    private func bind() {
        // メッセージが返ってきたらアラート表示
        viewModel.$message
            .filter { !$0.isEmpty }
            .sink { [unowned self] message in
                Task {
                    await showMessage(message)
                }
            }
            .store(in: &cancellables)
    }

    // アラートでメッセージを表示する
    private func showMessage(_ message: String) async {
        return await withCheckedContinuation { continuation in
            let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
            alert.addAction(.init(title: "OK", style: .default, handler: { _ in continuation.resume() }))

            present(alert, animated: true)
        }
    }
}
```

このコードを実行してモーダル画面を表示した後すぐ、 **アラートが表示される前に画面を閉じると、アプリがクラッシュ** します！

<img width="40%" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/25727/cc45b00b-212c-ec2a-3d9d-9854caee8138.gif">

このとき `Fatal error: Attempted to read an unowned reference but the object was already deallocated` というエラーログが出力されているので、どうも解放済の `self` 参照が発生していそうです。
もう少し詳細を知るために参照カウントを出力してみます。

```swift
    private func bind() {
        viewModel.$message
            .filter { !$0.isEmpty }
            .sink { [unowned self] message in
                print("ref count(begin): \(CFGetRetainCount(self))")
                Task {
                    // 🧨すぐにモーダル画面を閉じると、ここでクラッシュ
                    print("ref count(task): \(CFGetRetainCount(self))")
                    await showMessage(message)
                }
                print("ref count(end): \(CFGetRetainCount(self))")
            }
            .store(in: &cancellables)
    }
```

この状態で手順を踏むと、

```
ref count(begin): 2
ref count(end): 2
deinit
Fatal error: Attempted to read an unowned reference...
```

というログが出力されており、

* `ref count(end)` が `2` のままで、`Task` が作られた後に参照カウントが増えていない
* 先に `deinit` された後、 `Task` 内のクロージャを実行している

という挙動になっているので、意図しないクラッシュが起きてしまいそうです。

## 問題の原因と解決方法

先に述べたとおり `[unowned self]` でキャプチャしているクロージャ内 `Task` を生成すると、この問題が発生するようです。試しに以下のようなコード変更してみるとクラッシュが発生しません。

```swift
    private func bind() {
        viewModel.$message
            .filter { !$0.isEmpty }
            .sink { [unowned self] message in
                // メソッドを通じてTaskを実行する
                showMessage(message)
            }
            .store(in: &cancellables)
    }

    private func showMessage(_ message: String) {
        print("ref count(begin): \(CFGetRetainCount(self))")
        Task {
            print("ref count(task): \(CFGetRetainCount(self))")
            await showMessage(message)
        }
        print("ref count(end): \(CFGetRetainCount(self))")
    }
```

このときのログ出力は

```
ref count(begin): 3
ref count(end): 4
ref count(task): 2
deinit
```

となり、 `Task` が作られた後に参照カウントが増え、`Task` 内の非同期処理が実行されてから `deinit` されています。
一見すると同じ挙動になると思われるコードですが、書き方次第で最悪クラッシュしてしまうのはちょっと怖いですね...

> この参照カウントが増えない問題はコンパイラのバグなのか仕様なのか分からないので、何かご存知の方いましたら教えていただけると🙇‍♂️

## おまけ

アラートが表示される前にモーダル画面を閉じても、 `Task` はキャンセルせずインスタンスも生きているため、アラートを表示するロジックは実行されます。
しかし `presentingViewController` が存在しない状態になっているため、内部的に `Attempt to present ... whose view is not in the window hierarchy.` エラーが起き `continuation.resume()` がされず、メモリリークが発生してしまいます。

なので `presentingViewController` が `nil` でないことをチェックしておいたほうが良さそうです。

```swift
    private func showMessage(_ message: String) async {
        // これ何気に大事
        guard presentingViewController != nil else { return }

        return await withCheckedContinuation { continuation in
            let alert = UIAlertController(title: nil, message: message, preferredStyle: .alert)
            alert.addAction(.init(title: "OK", style: .default, handler: { _ in continuation.resume() }))

            present(alert, animated: true)
        }
    }
```