【Swift】SwifterがSwiftPMで動かない!

# はじめに

この記事は[【Swift】進捗をTwitterのアイコンに(Fleetっぽく)表示する](https://qiita.com/Enchan/items/62fd1796c882847f44f4) の続きです。
※内容的には分割されています

## 前回の反省

前回の記事の最後に、ちらっとこんなことを書いていました。

 > コールバック地獄のクソコード
 > RunLoopなんもわからん

記事では深く触れなかったのですが、実は[前回のコード](https://github.com/Enchan1207/GitGrassIndicator/blob/fea8eab4ad1fc7f22dda1fe371d7db43c7c59123/Sources/GitGrassIndicator/main.swift)はとんでもなく汚いものでした。

コールバックにコールバックが重なり、`RunLoop.main.run()`という**謎のおまじない**をファイル末尾に追加して無理矢理動かすなど、正直**こんなんでよく記事にしたな**と言われても仕方ないレベルの仕上がりとなってしまっていました。

今回はこのあたりの処理について**†完全に理解した†(非常に危険な発言)**ので、備忘録として残しておきます。

## SwiftPMでの非同期処理

SwiftPMで通常のアプリケーションと同様に非同期処理を走らせようとすると、若干の不都合が生じます。
例えば`URLSession`を動かそうとした場合…

```swift:URLSessionEx.swift
let url = URL(string: "https://example.com")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    if let error = error{
        fatalError(error.localizedDescription)
    }
    print(String(data: data!, encoding: .utf8)!)
}
task.resume()
```

```sh
$ swift run
# しかし　なにもおこらない
```

何も起こらずプログラムが終了してしまいます。 ~~至極当然です。~~
これはメインスレッドが非同期処理の完了を待機できていないことが原因です。

### `DispatchSemaphore`で処理の完了を待つ

[`DispatchSemaphore`](https://developer.apple.com/documentation/dispatch/dispatchsemaphore)を使用することで、この問題を解決することができます。

```swift:URLSessionWithSema.swift
let sema = DispatchSemaphore(value: 0)

let url = URL(string: "https://example.com")!
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
    if let error = error{
        fatalError(error.localizedDescription)
    }
    print(String(data: data!, encoding: .utf8)!)
    sema.signal() // 処理の終了を通知
}
task.resume()

sema.wait() // ここでメインスレッドがストップし、非同期処理の完了を待機してくれる
```

実行すると、リクエストが完了するまでしっかり待機してくれます。

```sh
$ swift run
<!doctype html>
<html>
<head>
...
</head>
<body>
...
</body>
</html>

Program ended with exit code: 0
```

## Swifterが動かない

あとはこれをSwifterにも適用すれば…と、思ったのですが。

```swift:SwifterEx.swift
import Swifter

let sema = DispatchSemaphore(value: 0)
let swifter = Swifter(...)
swifter.showUser(.id("1243395100387897347"), includeEntities: true) { (json) in
    print(json)
    sema.signal()
} failure: { (error) in
    print(error)
    sema.signal()
}
sema.wait()
```

```sh
$ swift run
# しかし　なにもおこらない
```

なにも起こりません。それどころか**プログラムが停止しないのです。**
さらにXCodeで見ると**ネットワーク通信すら行われていない**ことがわかります。
<img width="500" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/492914/b1d466ef-c087-111d-52bb-86dfb5a1e9b2.png">

さすがにこれはSwifterのバグだろうと思い(**大変失礼**)、issueを検索すると…

> Swifter is not reacting (#260)
> https://github.com/mattdonnelly/Swifter/issues/260

ありました。
collaboratorのmeteochu氏によると「`wait`の前に`RunLoop.main.run()`入れてや(意訳)」とのことだったので、該当行に挿入し実行してみたのですが…

```sh
$ swift run
...
SwifterError(message: "HTTP Status 400: Bad Request, ...", errorCode: 215)
```

ネットワーク通信は行われたものの、相変わらずプログラムが終了しません。

前回の記事ではここでお手上げになってしまい、**コールバック地獄の果てに`exit()`で逃げる**という原始的な方法で無理やり解決してしまっていました。

### 何故動かなかったのか

どうしても気になって仕方がなかったので、`Swifter.showUser()`の実装を調べてみることにしました。

まず`showUser`の定義は [SwifterUsers.swift](https://github.com/mattdonnelly/Swifter/blob/master/Sources/SwifterUsers.swift#L330-L343)に記述されています。

```swift:SwifterUsers.swift(引用)
func showUser(...) {
    ...
    self.getJSON(path: path, baseURL: .api, parameters: parameters, success: { json, _ in
        success?(json)
    }, failure: failure)
}
```

引数に渡されたコールバックを直接`getJSON()`の`success`, `failure`に渡していることがわかります。
この`getJSON`の実装は[Swifter.swift](https://github.com/mattdonnelly/Swifter/blob/master/Sources/Swifter.swift#L225-L236)に記述されています。

```swift:Swifter.swift(引用)
@discardableResult
internal func getJSON(...) -> HTTPRequest {

    return self.jsonRequest(path: path, baseURL: baseURL, method: .GET, parameters: parameters,
                            uploadProgress: uploadProgress, downloadProgress: downloadProgress,
                            success: success, failure: failure)
}
```

引数を`jsonRequest`に渡して`HTTPRequest`を作成し、returnしているようです。
`jsonRequest`の実装は[Swifter.swift](https://github.com/mattdonnelly/Swifter/blob/master/Sources/Swifter.swift#L154-L203)にあります。

```swift:Swifter.swift(引用)
@discardableResult
internal func jsonRequest(...) -> HTTPRequest {
    ...
    let jsonSuccessHandler: HTTPRequest.SuccessHandler = { data, response in
        DispatchQueue.global(qos: .utility).async {
            do {
                let jsonResult = try JSON.parse(jsonData: data)
                DispatchQueue.main.async {
                    success?(jsonResult, response)
                }
            } catch {
                DispatchQueue.main.async {
                    if case 200...299 = response.statusCode, data.isEmpty {
                        success?(JSON("{}"), response)
                    } else {
                        failure?(error)
                    }
                }
            }
        }
    }
    ...
```

`let jsonSuccessHandler: ...`以降の処理をよく読むと、

 - 優先度`.utility`の`DispatchQueue`で
 - JSONをパースし
 - **`DispatchQueue.main`で`success`を呼び出す**

ようになっています。
`URLSession`のコールバックと違い、**`Swifter`のコールバックはメインスレッドで実行される**設計になっているのです。(これは *UIの更新はメインスレッドで行われなければならない* という[UIKitの性質](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaFundamentals/AddingBehaviortoaCocoaProgram/AddingBehaviorCocoa.html)を見越した設計だと思われます)

これを踏まえて、もう一度先ほどのコードを読んでみます。

```swift:SwifterEx.swift
import Swifter

let sema = DispatchSemaphore(value: 0)
let swifter = Swifter(...)
swifter.showUser(.id("1243395100387897347"), includeEntities: true) { (json) in
    print(json)
    sema.signal()
} failure: { (error) in
    print(error)
    sema.signal()
}
sema.wait()
```

 - Swifterインスタンスを生成し
 - `showUser`を実行、コールバック内で`sema.signal`を呼び出すよう設定
 - **メインスレッドで**`sema.signal`が呼び出されるのを待つ

つまり、`showUser`が実行される頃にはメインスレッドはsemaを待機してしまっているのです。
**動くワケがありません。**

### 解決策

メインスレッドでsemaphoreを待機してしまっているのが問題なので、他の`DispatchQueue`で待機させるようにすれば想定通り動作します。

```swift:DispatchQueueを使った改善版
let swifter = Swifter(consumerKey: "", consumerSecret: "")

DispatchQueue.global().async {
    let sema = DispatchSemaphore(value: 0)
    swifter.showUser(.id("1243395100387897347"), includeEntities: true) { (json) in
        print(json)
        sema.signal()
    } failure: { (error) in
        print(error)
        sema.signal()
    }
    sema.wait()
    exit(EXIT_SUCCESS)
}

dispatchMain()
```

`dispatchMain`はメインスレッドを一時停止し、DispatchQueue.mainに積まれたブロックの処理に専念させる関数です。

> dispatchMain()
> 
> This function "parks" the main thread and waits for blocks to be submitted to the main queue.
>
> (引用:https://developer.apple.com/documentation/dispatch/1452860-dispatchmain)


```sh
$ swift run
[1/1] Merging module XXXXApp
SwifterError(message: "HTTP Status 400:....", ...  errorCode: 215)

$ # <- プログラムが正常に終了している!
```

ネットワーク通信も正常に行われ、非同期処理が完了したタイミングでプログラムが終了するようになりました。

## 最後に

ここまで読んでいただきありがとうございました。

改善版のソースコードは前回同様[GitHub](https://github.com/Enchan1207/GitGrassIndicator)にて公開しておりますので、「**結局何も理解してないじゃねーか!**」等ございましたらコメントまたはPRいただければ幸いです。

ではまた！