Closed12

Swiftで遊んでみる

上田小次郎上田小次郎

ずいぶん前にSwiftでもasync/awaitに対応したらしいので、試してみる。
昔、試しにアプリを作ってみたときはコールバック地獄だったように思う(そのせいでメモリリークしてた)けれど、それが改善されるならとても嬉しい。

上田小次郎上田小次郎

新規にプロジェクトをCommand Line Toolとして作っておいた。

async/awaitで通信で取得したデータをログに出してみる

これで通信した結果をログに吐くことができる。戻り値がある場合の関数もあとで作ってみる。

func fetch() async {
    let request = URLRequest(url: URL(string: "https://example.com")!)
    let session = try! await URLSession.shared.data(for: request)
    print(String(data: session.0, encoding: .utf8)!) // バイナリデータをテキストに変換して出力する
    print(session.1) // レスポンスヘッダーとかが入っている
}

呼び出しはこう

Task {
    await fetch()
}

で、実行してみたが、なんも起きない。
もしかして、バックグラウンド処理を実行する前に、プログラムが終了しているからかな?

上田小次郎上田小次郎

プログラムが終了しないように待ってみる

タスクが終わるまで、メインスレッドを終了しないようにすればいいはず。

Task {
    await fetch()
}
Thread.sleep(2)

に変えてみたら、動いた。
ただ、これだと、データの取得に2秒以上かかる場合にはやっぱりうまくいかないし、うまくいっても絶対に2秒かかるプログラムになってしまって、大変なので、処理が終わったら終了するようにしたい。

どうするんだろ。

上田小次郎上田小次郎

Swift で処理が終わるまで待つ

https://stackoverflow.com/questions/42484281/waiting-until-the-task-finishes

これによると、DispatchGroupで処理が終わるのを待つことができるらしい。JavaみたいにTaskそのものをjoinすることはできないのかな。
ともあれ、こうすればやりたいことは実現できる。

func runOnThread(task:@escaping  () async -> Void) {
    let group = DispatchGroup()
    group.enter()
    Task {
        await task()
        group.leave()
    }
    group.wait()
}

runOnThread {
    await fetch()
}
上田小次郎上田小次郎

JSONを取得して、オブジェクトにマッピングしてみる。

Swiftにはstructというデータ型があるようなので、それを使うっぽい。(Kotlinのdata classみたいなものかな)

func fetch() async {
    // ...
    print(try! JSONDecoder().decode(TodosJson.self, from: session.0)) // JSONDecoder で data 型を struct にマッピングする
    // ...
}

struct TodoJson: Decodable {
    var text: String = ""
    var done: Bool = false
}

struct TodosJson: Decodable {
    var todos: [TodoJson] = []
}

よさそう。

上田小次郎上田小次郎

ということで、ひとまず、CLIアプリケーションでasync/awaitを使用してデータの取得を行うことができた。

上田小次郎上田小次郎

ハック的なやり方でなく、await処理の終了を待つようにする

https://www.hackingwithswift.com/quick-start/concurrency/how-to-make-async-command-line-tools-and-scripts

こんな感じで、Javaでいうところのメインクラスを作って、そのクラスのメイン関数をasync対応すればよいらしい。これだと、 runOnThread が不要になる。

ちなみに、これで実行しようとすると、 main.swift というファイルだとコンパイルエラーになる。
どうやらmain.swiftは特別なファイルで、このファイルをメイン関数だと思って実行してくれるっぽい。しかし、メインクラスを書いてしまうと、メイン関数内にさらにメインクラスの定義があるぞ?ということでエラーとなるっぽい。

Swiftの正式な(?)やりかたで非同期処理を実装する方法がわかったといえそう。

上田小次郎上田小次郎

APIサーバーを立てる

Vaporというフルスタック?フレームワークがあるっぽいけど、やっぱりいちからプロジェクトを設定したいので、もう少し薄いライブラリを使いたい。可能であれば、HTTP通信を受けるところからやってみたい。

で、見つけたのが、 swifterというライブラリ。パスをいい感じにパースして、ルーティングする機能もあるし、最低限のAPIサーバーを立てる機能が揃っていそう。
中を見てみたけど、ソケットを開いてHTTP通信をハンドリングする、みたいな処理が書かれていたので、同じものをいちから作るのは結構めんどくさそうと判断。これを使ってみる。

上田小次郎上田小次郎

Swifterを使ってみた

let server = HttpServer()
server["/hello"] = { req in
        .ok(.text("hello world"))
}

try! server.start(8080)

簡単。シンプルに書けるし、よさそう。
と、思ったのだけれど、async/awaitでもあった、プログラムが終わっちゃう問題が再燃。
こっちの場合は server.start がasync関数なわけでもないので、 runOnThread のような処理が使えない。また、 Thread.sleep を使うとひとまず回避はできるのだけど、無限に待つようにしてもなんとなく正しい実装じゃない感があって気持ちが悪い。

上田小次郎上田小次郎

(正しく)無限に待つようにした

どうも、イベントループ(のようなもの)の処理を実行を行うようにするには、 dispatchMain という関数を実行すればいいらしい。実際、Swifterは内部でメインキューでHTTP通信を待ち受けているようなので、これが正解だと思う。waitInfinity みたいな関数を作らなくてよくて助かった・・・。

try! server.start(8080)
dispatchMain()

これにて、正しく無限に待つようになった。
でも、こういうの、ドキュメントに書いてくれてもいいと思うんだけどな。本来はiOS上でサーバー建てたいみたいな需要向けらしいので、書いてなくても困らないのもまあわかる。サーバーサイドSwiftに需要ないのだろうか。

別のフレームワーク(Vapor)でこんな議論があった。英語なのでよくわかんないけど、要はアプリケーション側が dispatchMain することを期待しているっぽい。わかりにくいからアイデアも提供している、って感じなのかな。
CLIだけでなく、iOSとかでも動作するように考慮すると、こうするのが今のところよい、という判断なのだろうか。

上田小次郎上田小次郎

Swifterをやめた

Swifterは同期処理しかできないので、非同期なasync/awaitと組み合わせてハンドラを書くことができない。一応、issueにそういう声は上がってるけど、あまり対応はしてくれなそう
では逆に、async/awaitな処理を同期処理で待つことはできないだろうか?と考えたのだけど、これはこれで難しそうだった。

そこで、ちょっと別のフレームワークに移行してみることにした。
Kituraにした。JSのフレームワークっぽい感じで、とてもとっつきやすい。async/awaitには対応していないけれど、非同期でレスポンスを返すことは可能なので(response.sendを使うタイプ)、まずはこれにしてみようと思う。

上田小次郎上田小次郎

Kituraもやめた

POSTのJSONボディを受けるときにstructにうまくマッピングできないのが理由。
content-typeを見て、application/jsonだったら、Dictionary型にマッピングしてくれるんだけど、そんなことするくらいだったらstructにマッピングしてほしいなと思う。Vaporならそこもいい感じにやってくれるので、Vaporを使うことにした。

この辺は実務に近いユースケースでつかってみることでわかることがありますね。

Vaporならasync/awaitにも対応しているので、とてもよい。

このスクラップは2023/04/13にクローズされました