swiftでHTTP通信


たまに忘れるので忘備録

HTTP通信ライブラリ EzHTTP [https://github.com/asaday/EzHTTP](https://github.com/asaday/EzHTTP) の使い方

## 概要

swiftでHTTP(S)通信といえばURLSessionです。
少々端折って書くとこんな感じですね

    URLSession.shared.dataTask(with: URL(string: "https://httpbin.org/get")!) { data, response, error in
        guard let d = data, let s = String(data: d, encoding: .utf8) else { return }
        print(s)
    }.resume()

とはいえもっと手短にわかりやすく書ける方がいいよねって用意ライブラリがこちら。
同じようなことを

    HTTP.get("https://httpbin.org/get") { r in
        print(r.stringValue)
    }

といった感じで書けます。簡単ですね。

基本的にURLSessionのラッパーなので処理内容は殆ど変わりません。

## Install

cocoapodsで

    pod 'EzHTTP'

で導入できるのですが Swift Package でも用意しているため Xcodeのみで追加出来ますので今回はこちらで。

Xcodeにて
File -> Swift Packages -> Add Package Dependecy...
と選択し、下記URLを入力します。

    https://github.com/asaday/EzHTTP.git

![Monosnap 2020-06-07 00-31-35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17516/47f842fe-27ec-d3af-3342-906739558982.png)


選択するとプロジェクトに追加されますので、あとはコード上に

    import EzHTTP

と記述で用意完了です。
iOSでもmacOSでもOKです。



## POST

GETですとQueryぐらいですが、POSTなどになってくると渡すデータの作成が少し大変になってきます。

GETは先に示しましたので
POSTで application/json として送付する場合を想定します。

URLSessionで書くと

        let param = ["key" : "value"]
        let url = URL(string: "https:/httpbin.org/post")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.httpBody = try! JSONSerialization.data(withJSONObject: param, options: [])
        URLSession.shared.dataTask(with: request) { data, response, error in
            guard let d = data, let s = String(data: d, encoding: .utf8) else { return }
            print(s)
        }.resume()

かなり端折ってこんな感じですが、それでもソラで書くには少し大変です。
EzHTTPで書くと

        HTTP.request(.POST, "https://httpbin.org/post", json: ["key": "value"]) { r in
            print(r.stringValue)
        }

となります。簡単ですね。
requestの各種方法は後述しますので、まずはresponseを。


## Response

通信結果となるresponseを見ていきます。
URLSession.shared.dataTask(..)のcallbackでのresponseは (Data?, URLResponse?, Error?) なのですが、EzHTTPではこれを1つのstructに統合し、少し利便性を高めるメソッドなどの追加拡張を行っています。

	struct Response: CustomStringConvertible {
		public let data: Data?
		public let error: NSError?
		public let response: HTTPURLResponse?
		....
    }

前例の r.stringValue の部分ですが、 r.data とするとURLSession.shared.dataTask(..)でのdataと同じ内容になります。
r.stringValueは URLSessoinの例で行っていた dataからstringへの変換を行った結果を返すメソッドとなっています。

method | Type | description
---|---|---
data | Data | URLSession.shared.dataTask(..)でのcallbackにある data
string | String? | dataをstringに変換したもの・変換エラー時はnil
jsonObject | NSObject? | dataを JSONSerialization.jsonObject(with: data, options:[]) したもの・エラー時nil
dataValue | Data? | dataの nil時に Data()とし、optionalを排除したもの
stringValue | String | stringの nil時 "" とし、optionalを排除したもの
jsonObjectValue | NSObject | jsonObjectの nil時 NSObject() とし、optionalを排除したもの
status | Int | response.statusCode 
description | String | 通信確認用のrequest,responseログ表示用文字列
request | URLRequest | request
duration | TimeInerval | 通信所要時間

RESTなどではjsonで返ってくることが多いですが、jsonは動的に引っ張り出すか(JSONSerializationで型当てしながらか、SwiftyJSONあたりが便利)、構造体にデコードして取り出しとなります。デコードについては後述します。

エラーに関しては status>=400 をHTTPでのエラーとして処理し、errorへ入れます。
URLSession通りにスルーする場合は illegalStatusCodeAsError = false としてください。

## Request

requestはURLRequestで行うのですが、これがちょっと機能不足気味で前述の通りPOSTでも面倒なことになります。
良く言えばHTTPの送信仕様なんていくらでも作れるのだから何とでも出来るように自由にしておけばいいのですが、自由度が高い代償として冗長になりミスを引き起こす要因にもなります。

EzHTTPでは定番のrequestに対してURLRequest作成を補助するラッパーを用意しました。
勿論URLRequestを別途作成してEzHTTPにて処理もできます。

Content-Typeから見た定番としては

- application/x-www-form-urlencoded

本来のHTTPでのPOSTにおけるデータ形式とでもいうのか、ブラウザ等でデータを送信するときのパターンですね。

- application/json

form-urlencodedでよかったもののjavascriptやらREST通信などアプリケーションから扱うのが面倒なのでjsonでいいじゃないで出てきたといっていいのでしょうか。

- multipart/form-data

ブラウザでのファイル送信がメインかな。
form-urlencodedではファイル等送信しにくいじゃないか、じゃあそれ自体を包んで複数置ければいいじゃないで出てきたといっていいのか。
このあたりまで来ると素で書くのは面倒です。

理由まで考えると泥沼の歴史が垣間見えそうですが、大体これぐらいのType押さえておけばよいかということで。


EzHTTPでは GETだけ別途用意していますが、基本的にはメソッド指定込みは以下のパターンでの引数を取ります。

    request(メソッド , URL , パラメタ系 , ヘッダ , コールバック)

### メソッド
enum定義していますので、 .GET なり .POST なりで指定出来ます。
	
    public enum Method: String { case OPTIONS, GET, HEAD, POST, PUT, PATCH, DELETE, TRACE, CONNECT }

### URL
URLとStringの両受けにしています。
Stringの場合はURLに自動変換します。

### パラメタ系
ざっくり param: ... json: ... body: ... の3種用意しています。

色々な方式に対応したため少々ややこしいですが基本的に以下となります。

- param フォームエンコード形式で送信
- json JSON形式で送信
- body 入力のまま送信


### ヘッダ
ヘッダに追加するリストです。
型は [String: String] となります。
不要なら省略してください。

その他にも色々ありますが、説明が大変なのでテストコードを参照してください。

URLRequrstには curlComand というメソッドを拡張しています。
print(request.curlComand) などするとrequestの内容をcurlでの引数として表示しますので、別途ターミナル等で検証する場合にコピペ出来るので少し便利です。


## Handler

通信ログを参照したい場合など通信動作の引き出しに使用します。
手っ取り早く使うには下記一文を最初に書いておけばよいです。
request,responseの内容を表示します。

    HTTP.shared.logHandler = HTTP.defaultLogHandler

ハンドラとしては以下があります。

	open var errorHandler: ResponseHandler?
	open var successHandler: ResponseHandler?
	open var logHandler: ResponseHandler?
	open var stubHandler: ((_ request: URLRequest) -> Response?)?
	open var retryHandler: ((_ result: Response) -> Bool)?
	open var indicatorHandler: ((_ visible: Bool) -> Void)? {

解説すると例などでかなり長くなりそうなので概要だけ。

- logHandler, successHandler, errorHandler

logHandlerは通信完了毎にその内容を伴ってコール。
successHandler,errorHandlerはそれぞれ通信成功,失敗時にコールします。

- indicatorHandler

通信してるかしてないかの判断用。
ステータスバーに表示される通信状況のために用意しましたが isNetworkActivityIndicatorVisible が iOS13 で Deprecated になってしまいましたね。

- stubHandler

APIがまだ無い時など通信せずにダミーデータなどを返す時に用います。
requestの内容で場合分けして自分でresponseを作って返すとその内容にて通信を返します。

- retryHandler

リトライしたい場合に使用します。
リトライカウントの処理なども考慮する必要がありますが、とりあえず3回版が用意されています。

HTTP.shared.retryHandler = HTTP.defaultRetryHandler


#### 補足

HTTP.sharedは EzHTTPにおける shared singleton です。
HTTP.request(...) などstaticなメソッドをコールするものは HTTP.shared.request(...)などへのショートカットとなっています。

タイムアウトやURLSessionConfigurationなどの共通設定は HTTP.shared に対して指定します。


## Sync

URLSessionを使う以上、結果の受け取りは非同期となりcallbackとなるのですが、
この通信をしてその結果でもってあの通信をして...となるとネストだらけのcallback地獄となります。
その解決として世の中色々あるのですがそのために別の沼にハマっていくことがあったりとか…通信のためにそんな事をする必要も無いだろうということで簡単に解決していきます。

同期で書ければ簡単なので同期版を用意しました。

    let r = HTTP.getSync("https://httpbin.org/get")
    print(r.stringValue)

メソッド名は...Syncとなります。返り値がcallbackで渡されていたものとなります。
その他は前述までの非同期版と同様です。

当然ながら同期の弊害として実行スレッドをブロックすることになりますのでメインスレッドで実行すると通信完了までの間止まってしまいます。
UIActivityIndicatorViewなどメインスレッドでブロックしていても見た目が動いているものなどでごまかすことも出来ますが、お試しならともかく実際に使う場合はメインスレッド以外にて実行してください。

    DispatchQueue.global(qos: .background).async {
        ... // 通信処理

        DispatchQueue.main.async {
            ... // 最後UIに返すなど
        }
    }


## Object Decode

通信結果がjsonだった場合、定義した構造体に値の取り出しをしたい場合があります。

前置きを書くと長くなるので例からで。

httpbin.orgは各種HTTPの操作をjsonで返してくれる便利サービスです。

GET https://httpbin.org/get?value=hello とした場合の結果はjsonで

    {
    "args": {
        "value": "hello"
    }, 
    "headers": {
        ...
    }, 
    "origin": "...", 
    "url": "https://httpbin.org/get?value=hello"
    }


といった感じで返してくれます。argsに注目して構造体を定義して通信結果をこれに返してみます。

    struct Result : Codable {
        struct Args : Codable {
            let value : String
        }
        let args: Args
    }

    HTTP.requestAndDecode(.GET, "https://httpbin.org/get?value=hello") { (result:Result?, response) in
        print(result?.args.value)
    }

はい、構造体に結果が返りました。
デコードエラーの場合は result=nil になります。

端折りすぎなので上記のものをもう少しバラして書くとこうなっています。

        HTTP.request(.GET, "https://httpbin.org/get?value=hello") { response in
            let result = ObjectDecoder().optionalDecode(Result.self, from: response.data)
            print(result?.args.value)
        }

dataをResultに変えているObjectDecoderがポイントでそれ以外は今までの話です。
swiftでのDecodableの機能を使っているのですが、仕組みや何故JSONDecoderではない等はこちらを参照して頂ければと。

https://qiita.com/nagisawks/items/3c484b1905dfb993b521

こちらにあるObjDecoderをEzHTTPに内包し利用しています。

jsonから構造体を定義するのは手動ですが、変換してくれるvscodeのプラグイン等色々あります。少し頑張ればAPIの定義から直接コードを出すスクリプトも用意できそうですね。


## ATS

もともとはHTTP(Sでない)通信は Info.plist の App Transport Security Settings でHTTP通信の許可を設定しなければいけないのですが、これを設定した場合リジェクトするようにするぞとAppleが言ってたわけで、それじゃあそれでもHTTP通信出来るようにしておこうと思ったから用意したわけなのですが、はい、4年経ってもそんなことはなく単なるブラフでした。
今やHTTPSもデファクトになったので、それはそれでよいのですが。

HTTP.shared.escapeATS = true 
とするとこの設定を必要な場合のみ回避してHTTP通信できます。

逃げ道としてはURLSessionを使わなければよい話で、じゃあどうすると言えばsocketでHTTP通信自体書けばいいじゃないかということでHTTPを簡易実装しました。
とはいえURLSessionの代替といった形でHTTPをやるにはそれなりにややこしいことが色々ありまして...コードは残してあるので参照してもらえればと。

### おまけ

その他色々ありますが、忘備録としてはこんなところで。

通信でデータの次は画像かなということで、EzHTTPを使って簡単に画像を表示出来る [EzImageLoader](https://github.com/asaday/EzImageLoader) というものも用意しています。

UIImageViewにURLを指定するだけで画像をダウンロードして来て表示出来ます。

        let iv = UIImageView(frame: view.bounds)
		view.addSubview(iv)
        iv.loadURL("https://httpbin.org/images/png")

簡単ですね。

jpeg, png, webp, gif, animation-gif, animation-png, animation-webp など読み込めます。

ファイル/メモリキャッシュなども行います。

機会があればまた。