標準ライブラリのみで簡単にAPIクライアントを実装する(Swift)

## はじめに

AlamofireやAFNetworking、SwiftyJSONなどサードパーティ製のライブラリを一切使わずにAPIクライアントを実装してみました。

そのようなライブラリは便利な反面、サイズが大きくなってパフォーマンスが低下したり、アップデートが手間などのデメリットもあります。

決して __使い方がよくわからなかったから採用しなかったわけではありません__ 。

## 環境

- OS：macOS High Sierra 10.13.1
- Xcode：9.2
- Swift：4.0.3

## APIクライアントの実装

GETメソッドのみ実装しています。
自分で必要になったらPOSTメソッドなども実装しようと思います。

```swift:HTTPClient.swift
import UIKit

/// HTTPクライアント
class HTTPClient: NSObject {
    // MARK: Enumerations
    
    /// HTTPメソッド
    ///
    /// - get: GETメソッド
    /// - post: POSTメソッド
    /// - put: PUTメソッド
    /// - delete: DELETEメソッド
    private enum HTTPMethod: String {
        case get = "GET"
        case post = "POST"
        case put = "PUT"
        case delete = "DELETE"
    }
    
    /// ヘッダー
    ///
    /// - contentType: Content-Type
    private enum HeaderField: String {
        case contentType = "Content-Type"
    }
    
    /// Content-Type
    ///
    /// - json: JSON
    private enum ContentType: String {
        case json = "application/json; charset=UTF-8"
    }
    
    // MARK: Properties
    
    // MARK: Public Methods
    
    /// GETメソッドを実行する
    ///
    /// - Parameters:
    ///   - urlString: URL文字列
    ///   - queriesDictionary: クエリディクショナリ
    /// - Returns: レスポンスボディ
    static public func get(urlString: String, queriesDictionary: [String: String]?) -> Any {
        let url = createUrl(urlString: urlString, queriesDictionary: queriesDictionary)
        return runHTTPMethod(requestBody: nil, url: url, method: .get, contentType: .json)
    }
    
    // MARK: Private Methods
    
    /// HTTPメソッドを実行する
    ///
    /// - Parameters:
    ///   - requestBody: リクエストボディ
    ///   - url: URL
    ///   - method: HTTPメソッド
    ///   - contentType: Content-Type
    /// - Returns: レスポンスボディ
    static private func runHTTPMethod(requestBody: [String: Any]?, url: URL, method: HTTPMethod, contentType: ContentType) -> Any {
        let request = createRequest(requestBody: requestBody, url: url, method: method, contentType: contentType)
        
        // HTTPメソッドを実行する
        var responseBody: Any! = nil
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
            do {
                // レスポンスボディをJSONからデシリアライズして返す
                responseBody = try JSONSerialization.jsonObject(with: data!, options: .allowFragments)
            } catch let error {
                print(error.localizedDescription)
                fatalError()
            }
        }
        task.resume()
        
        // レスポンスを待つ
        while responseBody == nil {
        }
        
        return responseBody!
    }
    
    /// リクエストを作成する
    ///
    /// - Parameters:
    ///   - requestBody: リクエストボディ
    ///   - url: URL
    ///   - method: HTTPメソッド
    ///   - contentType: Content-Type
    /// - Returns: リクエスト
    static private func createRequest(requestBody: [String: Any]?, url: URL, method: HTTPMethod, contentType: ContentType) -> URLRequest {
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        request.setValue(contentType.rawValue, forHTTPHeaderField:HeaderField.contentType.rawValue)
        
        // リクエストボディをディクショナリからJSONへシリアライズする
        if let _requestBody = requestBody {
            do {
                request.httpBody = try JSONSerialization.data(withJSONObject: _requestBody, options: [])
            } catch let error {
                print(error.localizedDescription)
                fatalError()
            }
        }
        
        return request
    }
    
    /// URLを作成する
    ///
    /// - Parameters:
    ///   - urlString: URL文字列
    ///   - queriesDictionary: クエリディクショナリ
    /// - Returns: URL
    static private func createUrl(urlString: String, queriesDictionary: [String: String]?) -> URL {
        var components = URLComponents(string: urlString)
        if let _queriesDictionary = queriesDictionary {
            components?.queryItems = createQueries(queriesDictionary: _queriesDictionary)
        }
        return (components?.url)!
    }
    
    /// クエリを作成する
    ///
    /// - Parameter queriesDictionary: クエリディクショナリ
    /// - Returns: クエリ配列
    static private func createQueries(queriesDictionary: [String: String]) -> [URLQueryItem] {
        var queries = [URLQueryItem]()
        for query in queriesDictionary {
            queries.append(URLQueryItem(name: query.key, value: query.value))
        }
        return queries
    }
}
```

## 使い方

例
郵便番号検索APIを実行し、ディズ○ーランドの住所を取得する

```swift:ZipCloudAPIClient.swift
import UIKit

/// ZipCloud APIクライアント
class ZipCloudAPIClient: NSObject {
    // MARK: Properties
    
    /// REST Endpoint URL
    /// 参考：http://zipcloud.ibsnet.co.jp/doc/api
    static private let restEndpointUrlString = "http://zipcloud.ibsnet.co.jp/api/search"
    
    // MARK: Public Methods
    
    static public func searchDisneylandAddress() {
        let disneylandAddress: [String: Any] = getAddress(zipcode: "279-0031")
        let results = disneylandAddress["results"] as! [[String: Any]]
        let result = results[0]
        
        print(result)
    }
    
    // MARK: Private Methods
    
    static private func getAddress(zipcode: String) -> [String: Any] {
        let queries = ["zipcode": zipcode]
        return HTTPClient.get(urlString: restEndpointUrlString, queriesDictionary: queries) as! [String: Any]
    }
}
```

```json:searchDisneylandAddress()の実行結果
["kana2": ｳﾗﾔｽｼ, "address1": 千葉県, "address3": 舞浜, "address2": 浦安市, "prefcode": 12, "kana1": ﾁﾊﾞｹﾝ, "zipcode": 2790031, "kana3": ﾏｲﾊﾏ]
```

ちなみにこのAPIを実行できるようになるまでに以下の2記事が書き上がりました。。

- [「http://」のAPIを実行できるようにする(Swift) - Qiita](https://qiita.com/uhooi/items/68939999c2c31e5f5557)
- [GETメソッドでリクエストボディを指定してはいけない(Swift) - Qiita](https://qiita.com/uhooi/items/e82c8d294a8465a3e6f3#_reference-1012bdd8673e590c81a3)

## おわりに

Swiftに慣れていないため、適切でない実装箇所があると思います。(特に例外処理や `enum` の使い方)
お気づきになりましたら、遠慮なくコメントや編集リクエストなどを頂けると嬉しいです。

あとすでにお気づきでしょうが、冒頭に伝えた言葉の一部に嘘があります。
__Alamofireなどの使い方がよくわからなかったから自作しました__ 。

でも __学習コストがかかる__ のもライブラリ導入のデメリットではあるので、あながち間違った方向性ではないと思います。

「Alamofireを使えばもっとよく書ける！」という意見も大歓迎です。