概要
タイトルそのままですが、Swiftの@dynamicCallable
を使う上で実装が必要になるdynamicallyCall
で、async/awaitが使えます。
dynamicCallableについて
dynamicCallableはSwift 5で追加された機能です。
@dynamicCallable
がマークされた型のインスタンスは、好きな名前で引数をつけて関数呼び出しが可能です。
@dynamicCallable
がマークされた型はdynamicallyCall(withArguments:)
もしくはdynamicallyCall(withKeywordArguments:)
のfuncを実装します。
dynamicallyCallは、引数の名前によって動作させることができます。dynamicallyCallは好きな返り値を設定することができます。
@dynamicCallable
struct Foo {
func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, String?>) -> String? {
pairs.first.map {
"\($0.key):\($0.value ?? "")"
}
}
}
let foo = Foo()
print(foo(bar: "hoge") ?? "") //bar:hoge
本題
async/awaitを利用する
このdynamicCallableは好きな返り値を設定できると言いましたが、試してみたらasync/awaitも使えました。調べた限りドキュメント等に書かれているわけではないですが、インスタンス呼び出しの際のパラメータで非同期で呼び出せます。
非同期で呼び出すといえばWeb APIなので、Web APIの実行例としてQiita APIで試してみます。
Web APIの仕様に合わせてdynamicCallableをasync/awaitで組み立てる(Qiita編)
まずはQiita API v2の仕様について見ていきましょう。
APIとの全ての通信にはHTTPSプロトコルを利用します。アクセス先のホストには、Qiitaのデータを利用する場合には qiita.com を利用し、Qiita Teamのデータを利用する場合には *.qiita.com を利用します (*には所属しているTeamのIDが入ります)。
とあるので、URLのscheme部分は"https"で固定ですが、hostに関してはQiitaとQiita Teamでは異なることがわかります。
なのでdynamicCallableに対応したクラスはhostをパラメータとして受け入れられる形にします。
こんな形でQiita
クラスを作成し、配下に@dynamicCallable
付きのGet
クラスを作ります。
そして引数の部分は、名前のない場合はpath、名前のある引数はqueryパラメータとしてURLを組み立てて、URLSessionでネットワーク通信を行うものを作成してみます。結果はtry await
で受け取ります。
struct Qiita {
internal init(host: String) {
self.get = Get(host: host)
}
private static let scheme = "https"
let get: Get
@dynamicCallable
struct Get {
let host: String
func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, String?>) async throws -> (Data, URLResponse) {
var components = URLComponents()
components.scheme = Qiita.scheme
components.host = host
components.path = (pairs.filter { $0.key.isEmpty }.first?.value)!
components.queryItems = pairs.filter({ !$0.key.isEmpty}).map({
URLQueryItem(name: $0.key, value: $0.value)
})
return try await URLSession.shared.data(from: components.url!)
}
}
}
使う
実際にQiitaクラスを使用してみます。
まずはタグの取得を行なってみます。
GET /api/v2/tagsの仕様を見て、
page
ページ番号 (1から100まで)
Example: 1
Type: string
Pattern: /^[0-9]+$/
per_page
1ページあたりに含まれる要素数 (1から100まで)
Example: 20
Type: string
Pattern:/^[0-9]+$/
sort
並び順 (countで記事数順、nameで名前順)
Example: "count"
Type: string
とあるので
let qiita = Qiita(host: "qiita.com")
Task {
let (data, _) = try await qiita.get("/api/v2/tags",
page: "11",
per_page: "3",
sort: "count")
print(String(data: data, encoding: .utf8)!) // レスポンスデータ
}
こんな感じでtry await
を使って実際にQiita上のタグのデータを使うことができます。
記事の取得など、違うAPIを使いたい場合もパラメータを変えるだけで済みます。
Task {
let (data, _) = try await qiita.get("/api/v2/items",
page: "1",
per_page: "3",
query: "qiita user:Qiita")
print(String(data: data, encoding: .utf8)!) // レスポンスデータ
}