DispatchGroupを使っていたら、「Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)」が発生

SwiftのDispatchなんちゃらで毎回詰まっていますが、今回もまた詰まったので
記録に残しておきます。

多分記事にしておくくらいの苦労をしないと、また数ヶ月後にまた同じことで詰まる気がとてもしているからです！
（って自信持っていうことじゃないが笑）

原因は単純ミスです。

# 発生していたこと

DispatchGroupを使用していたところ、
`Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)`
というエラーが発生・・・

実際にプロジェクトで詰まったコードに似たサンプルを作りました。
Playgroundで実行できます。

`Thread.sleep(forTimeInterval: 0.5)`の部分は実際はAPIがリクエストされている箇所でしたが、
今回はsleepに差し替えています。

```swift
import UIKit

func getItems(
    itemIds: [String],
    completionHandler: @escaping ([String]) -> Void
) {
    var items = [String]()
    let group = DispatchGroup()
    
    for id in itemIds {
        group.enter()
        getItemDetail(id) { result in
            defer {
                group.leave()
            }
            guard let result = result else {
                return
            }
            items.append(result)
            print("ループの中")
        }
    }
    
    group.notify(queue: .main) {
        print("ループ全部終わった\n-------------")
        completionHandler(items)
    }
}

func getItemDetail(
    _ id: String,
    completionHandler: @escaping (String?) -> Void
) {
    Thread.sleep(forTimeInterval: 0.5)
    print("0.5秒待機中")
    calcPrice(id) { result in
        completionHandler(id)
    }
}

func calcPrice(
    _ id: String,
    completionHandler: @escaping (String?) -> Void
) {
    if !id.isEmpty {
        Thread.sleep(forTimeInterval: 0.5)
        completionHandler(id)
    }
    completionHandler(nil)
}

// -----------------------------------

let itemIds = ["1", "2", "3", "", ""]

getItems(itemIds: itemIds) { result in
    print(result)
}

```

# どう解決したか、原因は？

エラーメッセージからは何が原因かわかりませんでしたが、[参考](##参考)に載せたstackoverflowで、この答えを発見。

> You can check number of count entered in group before leaving from any group by below Patch Work

また、コメントを見ていたところ、こんな記述も発見。

> My API call was sending 2 completion handlers

ということではい、私もドンピシャで同じことをしておりました。

修正したのは`calcPrice`メソッドです。
`return`を忘れた結果、completionHandlerが2回呼ばれてしまっていました。

```swift
func calcPrice(
    _ id: String,
    completionHandler: @escaping (String?) -> Void
) {
    if !id.isEmpty {
        Thread.sleep(forTimeInterval: 0.5)
        completionHandler(id)
        return  // 追加したコード
    }
    completionHandler(nil)
}
```

公式ドキュメントにも似たようなことが書いてありました。

> A call to this function must balance a call to enter(). It is invalid to call it more times than enter(), which would result in a negative count.

つまり`enter()`と`leave()`の呼び出し回数が釣り合ってなかったわけです。
`itemIds`の要素が空文字でない場合、`enter()`が1回呼ばれているのに対し、`leave()`が2回呼ばれていました。

それが原因で例のエラーが起きていました。

コード全体はこちらに上がっています。

https://github.com/kamimi01/iOSTroubleShooting/tree/main/DispatchGroupTest.playground

# おわりに

`return`を忘れるという単純ミスでしたが、なかなか気づかなかったので
記事として記録しておきました。

Swift自体も勉強中ですが、中でも非同期処理が苦手です。
もっと良いやり方あるよ、ここ違くないなどなどありましたが、ご指摘いただけますと幸いです。 :blush: 

# 参考

- [Apple Developer Document leave()](https://developer.apple.com/documentation/dispatch/dispatchgroup/1452872-leave)→公式ドキュメント
- [Call DispatchGroup() in the right place](https://stackoverflow.com/questions/48263776/call-dispatchgroup-in-the-right-place)→stackoverflow
- [Leaving DispatchGroup causes my code to crash](https://stackoverflow.com/questions/51299012/leaving-dispatchgroup-causes-my-code-to-crash)→stackoverflow