0
1

More than 1 year has passed since last update.

@dynamicCallableのdynamicallyCallはasync/awaitでも使える

Last updated at Posted at 2022-12-08

概要

タイトルそのままですが、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)!) // レスポンスデータ
    }
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