LoginSignup
0
1

More than 3 years have passed since last update.

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

Posted at

はじめに

この記事は【Swift】進捗をTwitterのアイコンに(Fleetっぽく)表示する の続きです。
※内容的には分割されています

前回の反省

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

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

記事では深く触れなかったのですが、実は前回のコードはとんでもなく汚いものでした。

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

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

SwiftPMでの非同期処理

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

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()
$ swift run
# しかし なにもおこらない

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

DispatchSemaphoreで処理の完了を待つ

DispatchSemaphoreを使用することで、この問題を解決することができます。

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() // ここでメインスレッドがストップし、非同期処理の完了を待機してくれる

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

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

Program ended with exit code: 0

Swifterが動かない

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

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()
$ swift run
# しかし なにもおこらない

なにも起こりません。それどころかプログラムが停止しないのです。
さらにXCodeで見るとネットワーク通信すら行われていないことがわかります。

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

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

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

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

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

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

何故動かなかったのか

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

まずshowUserの定義は SwifterUsers.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に記述されています。

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にあります。

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: ...以降の処理をよく読むと、

  • 優先度.utilityDispatchQueue
  • JSONをパースし
  • DispatchQueue.mainsuccessを呼び出す

ようになっています。
URLSessionのコールバックと違い、Swifterのコールバックはメインスレッドで実行される設計になっているのです。(これは UIの更新はメインスレッドで行われなければならない というUIKitの性質を見越した設計だと思われます)

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

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で待機させるようにすれば想定通り動作します。

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)

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

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

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

最後に

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

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

ではまた!

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1