概要
Swiftの機能である @dynamicMemberLookup
と @dynamicCallable
を使って、いい感じにWeb APIにアクセスしてみます。
試しにQiitaのAPIにアクセスしようと思います。
#@dynamicCallable
でパラメータ作成
GET
GETアクセスをするためには、queryパラメータを設定する必要があります。
URLComponentsを作成してqueryItemsにURLQueryItemを設定することでリクエストするためのURLが作成できます。
URLQueryItemを @dynamicCallable
で作成する
以下のような形でURLQueryItemsGenerator
を定義してみました。
@dynamicCallable
struct URLQueryItemsGenerator {
func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, String?>) -> [URLQueryItem] {
pairs.map { (key, value) -> URLQueryItem in
URLQueryItem(name: key, value: value)
}
}
}
GET URLの作成
このURLQueryItemsGeneratorを作成し、(1)
作成したジェネレーターにAPIドキュメント通りにパラメータを与えると、(2)
URLQueryItemが簡単に作れ、URLにqueryパラメータを与えることができます。
var components = URLComponents()
components.scheme = "https"
components.host = "qiita.com"
components.path = "/api/v2/items"
let generator = URLQueryItemsGenerator() // (1)
components.queryItems = generator(page: "2",
per_page: "10",
query: "SwiftUI") // (2)
https://qiita.com/api/v2/items?page=2&per_page=10&query=SwiftUI
POST
QiitaのPOST APIはほとんど Content-Type: application/json
なので、POST用のbodyのJSON Dataを生成するジェネレータを作ります。
JSON Dataを @dynamicCallable
で作成する
以下のような形でdynamicCallableを定義してみました。
Dictionaryを作成するDictionaryGenerator
とJSONのデータを作成するJSONDataGenerator
です。
@dynamicCallable
struct DictionaryGenerator {
func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, Any?>) -> [String:Any] {
var dictionary:[String:Any] = [:]
pairs.forEach { (key, value) in
dictionary[key] = value
}
return dictionary
}
}
@dynamicCallable
struct JSONDataGenerator {
func dynamicallyCall(withKeywordArguments pairs: KeyValuePairs<String, Any?>) -> Data? {
let generator = DictionaryGenerator()
let dictionary = generator.dynamicallyCall(withKeywordArguments: pairs)
guard JSONSerialization.isValidJSONObject(dictionary) else {
return nil
}
return try! JSONSerialization.data(withJSONObject: dictionary, options: [])
}
}
POST Requestの作成
JSONDataGenerator
を作成し、作成したジェネレーターにAPIドキュメント通りにパラメータを与えると、POST Request用のJSON Dataが作成できます。
/api/v2/itemsのtagsパラメータにはオプジェクトの配列が必要なので、DictionaryGenerator
も使ってhttpBodyにJSON Dataを設定しています。
var components = URLComponents()
components.scheme = "https"
components.host = "qiita.com"
components.path = "/api/v2/items"
var request = URLRequest(url: components.url!)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
let jsonDataGenerator = JSONDataGenerator()
let dictionaryDataGenerator = DictionaryGenerator()
request.httpBody = jsonDataGenerator(body:"# Example",
private:"false",
tags:[
dictionaryDataGenerator(name:"Ruby",
versions:["0.0.1"])
],
title: "Example title",
tweet: false)
{
"body": "# Example",
"private": false,
"tags": [
{
"name": "Ruby",
"versions": [
"0.0.1"
]
}
],
"title": "Example title",
"tweet": false
}
#@dynamicMemberLookup
でJSON受け取り
リクエストが作成できたので、リクエストをAPIに送信してJSONレスポンスを受け取ります。
JSON dynamicMemberLookup
だいぶ長いですが、以下のように@dynamicMemberLookup
を付与したJSON解析用のenumを用意します。
@dynamicMemberLookup
enum JSON {
case dictionaryValue(Dictionary<String, JSON>)
case arrayValue(Array<JSON>)
case numberValue(NSNumber)
case stringValue(String)
case boolValue(Bool)
case nullValue
var objectValue: Dictionary<String, JSON>? {
if case .dictionaryValue(let dictionary) = self {
return dictionary
}
return nil
}
var arrayValue: Array<JSON>? {
if case .arrayValue(let array) = self {
return array
}
return nil
}
var stringValue: String? {
if case .stringValue(let str) = self {
return str
}
return nil
}
var numberValue: NSNumber? {
if case .numberValue(let number) = self {
return number
} else if case .boolValue(let b) = self {
return NSNumber(value: b)
}
return nil
}
var boolValue: Bool? {
if case .boolValue(let bool) = self {
return bool
}
return nil
}
var nullValue: NSNull? {
if case .nullValue = self {
return NSNull()
}
return nil
}
subscript(index: Int) -> JSON? {
if case .arrayValue(let array) = self {
return index < array.count ? array[index] : nil
}
return nil
}
subscript(dynamicMember member: String) -> JSON? {
if case .dictionaryValue(let dict) = self {
return dict[member]
}
return nil
}
private init(_ object: Any) {
switch object {
case let boolValue as Bool: self = .boolValue(boolValue)
case let numberValue as NSNumber: self = .numberValue(numberValue)
case let stringValue as String: self = .stringValue(stringValue)
case let dictionaryValue as Dictionary<String, Any>: self = JSON.dictionaryValue(dictionaryValue.mapValues{ JSON($0) })
case let arrayValue as Array<Any>: self = .arrayValue(arrayValue.map { JSON($0)} )
default: self = .nullValue
}
}
init(data: Data) throws {
let jsonObject = try JSONSerialization.jsonObject(with: data, options: [])
self = .init(jsonObject)
}
}
APIにアクセスして記事を取得
先ほどのURLQueryItemsGeneratorも組み合わせてAPIにアクセスして記事のタイトルを取得します。
以下のように、indexやドットを利用して、先頭の記事タイトルが取れます。
var components = URLComponents()
components.scheme = "https"
components.host = "qiita.com"
components.path = "/api/v2/items"
let generator = URLQueryItemsGenerator()
components.queryItems = generator(page: "2",
per_page: "10",
query: "SwiftUI")
let task = URLSession.shared.dataTask(with: components.url!) { (data, response, error) in
let json = try! JSON(data: data!)
print(String(describing: json[0]?.title?.stringValue)) // 先頭の記事のタイトルが取れる
}
task.resume()
#なにがいいのか
API処理でクライアントを用意したり、Codableなどでレスポンスの定義をきっちりと行って処理をすることも多いんじゃないかと思いますが、@dynamicMemberLookup
や@dynamicCallable
を使うと、その辺りのコードが全て削れます。
外部のAPIと連携するようなアプリの場合、API側の仕様変更が行われたら、APIクライアントやCodableのメンテナンスが必要になりますが、@dynamicMemberLookup
や@dynamicCallable
を使うと、リクエスト送信処理やレスポンス受信処理だけAPIのドキュメントとにらめっこしてドットアクセスやパラメータ作成をするだけですむので、そこが気に入ってます。
#おまけ
今回のコードをまとめたものをSwift Package Managerとして提供しています。是非使ってください。スターください。
https://github.com/coe/Dynamics