iOSでライブラリに頼らず、URLSessionを使ってHTTP通信する

これは [Swift Advent Calendar 2016](http://qiita.com/advent-calendar/2016/swift) 16日目の投稿です。
昨日15日目は[@_ha1f](http://qiita.com/_ha1f)さんの[Photos.frameworkのサンプルを読む](http://qiita.com/_ha1f/items/6ae8ece70107f2944a78)でした。

# TL;DR
Multipart頻発したり、大きなデータのダウンロードがメインのアプリではなく、**単純なGet, Postのみなら、URLSessionで問題ない**

# 背景
今までiOSでの通信処理といえば、当たり前にAFNetworkingかAlamofire (その他のライブラリも触ったことはありますが) を使っており、お恥ずかしながら、標準APIでの処理を書いたことがありませんでした。「Swift 通信」で検索すると1番目にAlamofireを使った記事がヒットすることからも、自分のような人は意外と多いのではないでしょうか？

また、[mono0926](http://qiita.com/mono0926)さんの[iOS 10が正式リリースされた今、そろそろ既存アプリのiOS 8サポートは切っても良いだろうかという考察](http://qiita.com/mono0926/items/776a79a830570fbe683c#alamofire-40がios-90以上のみのサポートになった)の`Alamofire 4.0がiOS 9以上のみサポートになった`の項や[@yimajo](http://qiita.com/yimajo)さんの[今から新規でiOSアプリを書き始めるなら。2016年冬](http://qiita.com/yimajo/items/5a7333fd709e625b60da#alamofire)の`Alamofire`の項を読んで、確かに「あんなに巨大で全体を把握しづらいライブラリを安易に使っていていいのか？」、「そもそも最初からライブラリありきで思考停止してるのでは？」と思いました。

そこで今回は標準で提供されているAPIで通信を行なってみて、本当に通信ライブラリが必要なのかを考えてみました。
[@codelynx](http://qiita.com/codelynx)さんの[URLSessionDownloadTask でちょっと大きめなデータでも寝ている間にダウンロード](http://qiita.com/codelynx/items/f4ff935058addaa9c5d7)と一部被っている感じもありますが、ご容赦ください。

**APIのサンプルは`https://httpbin.org`をお借りしました。**

## Playgroundで通信する
今回は簡単に検証するためにPlaygroundを使用します。

```swift
import PlaygroundSupport
 
PlaygroundPage.current.needsIndefiniteExecution = true
```
初めて知りました。(参考: http://dev.classmethod.jp/smartphone/iphone/swift-3-playground-urlsession/)

# Get
```swift
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true


class URLSessionGetClient {
  
  func get(url urlString: String, queryItems: [URLQueryItem]? = nil) {
    var compnents = URLComponents(string: urlString)
    compnents?.queryItems = queryItems
    let url = compnents?.url
    let task = URLSession.shared.dataTask(with: url!) { data, response, error in
      if let data = data, let response = response {
        print(response)
        do {
          let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
          print(json)
        } catch {
          print("Serialize Error")
        }
      } else {
        print(error ?? "Error")
      }
    }
    
    task.resume()
  }
  
}

let urlSessionGetClient = URLSessionGetClient()
let queryItems = [URLQueryItem(name: "a", value: "foo"),
                  URLQueryItem(name: "b", value: "1234")]
urlSessionGetClient.get(url: "https://httpbin.org/get", queryItems: queryItems)

```
APIのURLとリクエストするパラメータを`?a=xxx&b=yyy&c=zzz`と連結していくだけと、非常に単純です。
~~parametersStringの辺りはきちんとPatternMatchを行う等の改善の余地がありますが。~~
※2017/07/11追記
[@sawat1203](http://qiita.com/sawat1203)さんにコメント頂き、[URLComponents](https://developer.apple.com/documentation/foundation/urlcomponents)と[URLQueryItem](https://developer.apple.com/documentation/foundation/urlqueryitem)を使ったパラメーターのセット方法に修正しました。
Getのメソッドの引数に `[URLQueryItem]` 渡すようにすると、簡潔に書くことができますね！

Getの場合はURLSessionで十分簡潔に書くことができました。
`URLSession.shared` ではなく、`let session = URLSession(configuration:  URLSessionConfiguration)`を使う場合は、sessionが強参照で保持されるので、completionHandlerの内部で`session.invalidateAndCancel()` or `session.finishTasksAndInvalidate()`として、適宜解放する必要があります。



##### ちなみにURLConnection (iOS 9でdeprecated) で書くとこんな感じ

```swift
class URLConnectionClient {
    
    func get(url urlString: String) -> Void {
        let url = URL(string: urlString)
        let request = URLRequest(url: url!)
        let queue = OperationQueue.main
        NSURLConnection.sendAsynchronousRequest(request, queue: queue) { response, data, error in
            if let response = response, let data = data {
                print(response)
                do {
                    let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
                    print(json)
                } catch {
                    print("")
                }
            } else {
                print(error ?? "")
            }
        }
    }
    
}

let urlConnectionClient = URLConnectionClient()
urlConnectionClient.get(url: "https://httpbin.org/get")
```
responseのHandlerの引数の順番が、`(URLResponse?, Data?, Error?)` -> `(Data?, URLResponse?, Error?)` に変更になってますね。

# Post
```swift
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class URLSessionPostClient {

    func post(url urlString: String, parameters: [String: Any]) {
        let url = URL(string: urlString)
        var request = URLRequest(url: url!)
        request.httpMethod = "POST"
        
        let parametersString: String = parameters.enumerated().reduce("?") { (input, tuple) -> String in
            switch tuple.element.value {
            case let int as Int: return input + tuple.element.key + "=" + String(int) + (parameters.count - 1 > tuple.offset ? "&" : "")
            case let string as String: return input + tuple.element.key + "=" + string + (parameters.count - 1 > tuple.offset ? "&" : "")
            default: return input
            }
        }
        
        request.httpBody = parametersString.data(using: String.Encoding.utf8)
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data, let response = response {
                print(response)
                do {
                    let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
                    print(json)
                } catch {
                    print("Serialize Error")
                }
            } else {
                print(error ?? "Error")
            }
        }
        task.resume()
    }

}

let urlSessionPostClient = URLSessionPostClient()
urlSessionPostClient.post(url: "https://httpbin.org/post", parameters: ["a": "foo", "b": 1234])
```
Postの場合はパラメータがnilの状況が考えられなかったので、Optionalを外しました。
パラメータはエンコードして、httpBodyに入れます。
PostもGetの時とほぼ変わらずに書けたように思います。

# Multipart
こいつが厄介です。今回はユーザーID, トークン, 画像(jpeg)の送信を想定しました。

```swift
import UIKit
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class URLSessionMulitipartClient {
 
    func mulipartPost(url urlString: String, parameters: [String: Any]) {
        let url = URL(string: urlString)
        var request = URLRequest(url: url!)
        request.httpMethod = "POST"
        
        let uniqueId = ProcessInfo.processInfo.globallyUniqueString
        let boundary = "---------------------------\(uniqueId)"
        
        // Headerの設定
        request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
        
        // Bodyの設定
        var body = Data()
        var bodyText = String()
        
        for element in parameters {
            switch element.value {
            case let image as UIImage:
                let imageData = UIImageJPEGRepresentation(image, 1.0)
                bodyText += "--\(boundary)\r\n"
                bodyText += "Content-Disposition: form-data; name=\"\(element.key)\"; filename=\"\(element.key).jpg\"\r\n"
                bodyText += "Content-Type: image/jpeg\r\n\r\n"
                
                body.append(bodyText.data(using: String.Encoding.utf8)!)
                body.append(imageData!)
            case let int as Int:
                bodyText = String()
                bodyText += "--\(boundary)\r\n"
                bodyText += "Content-Disposition: form-data; name=\"\(element.key)\";\r\n"
                bodyText += "\r\n"
                
                body.append(bodyText.data(using: String.Encoding.utf8)!)
                body.append(String(int).data(using: String.Encoding.utf8)!)
            case let string as String:
                bodyText += "--\(boundary)\r\n"
                bodyText += "Content-Disposition: form-data; name=\"\(element.key)\";\r\n"
                bodyText += "\r\n"
                
                body.append(bodyText.data(using: String.Encoding.utf8)!)
                body.append(string.data(using: String.Encoding.utf8)!)
            default:
                break
            }
        }
        
        // Footerの設定
        var footerText = String()
        footerText += "\r\n"
        footerText += "\r\n--\(boundary)--\r\n"
        
        body.append(footerText.data(using: String.Encoding.utf8)!)
        
        request.httpBody = body
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let data = data, let response = response {
                print(response)
                do {
                    let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)
                    print(json)
                } catch {
                    print("Serialize Error")
                }
            } else {
                print(error ?? "Error")
            }
        }
        
        task.resume()
    }
    
}

let urlSessionMultipartClient = URLSessionMultipartClient()
let parameters = ["sample": #imageLiteral(resourceName: "fox"), "userId": 1234, "accessToken": "xxxxxxxxxxxxxxx"] as [String : Any] // 画像は適宜Playgroundへ
urlSessionMultipartClient.mulipartPost(url: "https://httpbin.org/post", parameters: parameters)
```
フォームデータを形成する部分は入り組んでいますが、読めなくもない...
画像の拡張子はjpegを想定しましたが、APIが要求するフォーマットが複数出てくると`parameters` をDictionaryにすることが難しくなりますね。HttpBodyを表現するための型を自作することになるかと思います。 (実際Alamofireの場合は、`MultipartFormData`という型が使用されています。)
久々に [Webを支える技術](http://amzn.asia/f5j3U4v) を引っ張り出しました。

# Download
大きなデータのダウンロードには、今までの `dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void) -> URLSessionDataTask` ではなく、 `downloadTask(with url: URL) -> URLSessionDownloadTask` を使用します。

```swift
import UIKit
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class URLSessionDownloadImageClient: NSObject, URLSessionDownloadDelegate {
    
    func downloadImage(url urlString: String) -> Void {
        
        let url = URL(string: urlString)!
        let configuration = URLSessionConfiguration.background(withIdentifier: "backgroundSessionConfiguration")
        let session = URLSession(configuration: configuration,
                                 delegate: self,
                                 delegateQueue: OperationQueue.main)
        let task = session.downloadTask(with: url)
        task.resume()
    }
    
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        do {
            let data = try Data(contentsOf: location)
            let image = UIImage(data: data)
        } catch {
            print("Serialize Error")
        }
    }
    
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        
        session.invalidateAndCancel()
        
        if let error = error {
            print(error)
            return
        }
    }
    
}

let urlSessionDownloadImageClient = URLSessionDownloadImageClient()
urlSessionDownloadImageClient.downloadImage(url: "https://httpbin.org/image/png")
```

[URLSessionDownloadTask でちょっと大きめなデータでも寝ている間にダウンロード](http://qiita.com/codelynx/items/f4ff935058addaa9c5d7)で[@codelynx](http://qiita.com/codelynx)さんも仰っていますが、Closureでハンドル出来ないため、実際に使用する際はさらに複雑になるかと思います。

# HTTP Status Code
HTTP通信をする以上、HTTP Status Codeのハンドリングもする必要が出て来るかと思います。
Status Codeは `URLResponse` を `HTTPURLResponse` にダウンキャストすることで取得できます。

```swift
import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class URLSessionClient {
    
    func get() {
        let url = URL(string: "https://httpbin.org/get")
        let task = URLSession.shared.dataTask(with: url!) { data, response, error in
            if let response = response {
                let statusCode = (response as! HTTPURLResponse).statusCode
                print(statusCode)
            }
        }
        
        task.resume()
    }

}

let urlSessionClient = URLSessionClient()
urlSessionClient.get()
```
Status Code取得後のエラー処理は自前で行います。
きちんとしたエラー処理は、HTTP通信のみならず、永遠の悩みな気がしますが...


# まとめ
* Get: URLSessionで問題なし
* Post: URLSessionで問題なし
* Multipart: URLSessionでは複数パターン対応が大変になるが、できないこともない
* Download: 多くのダウンロードが発生すると辛くなってくる
* HTTP Status Code: 取得は容易にできるが、その後のエラー処理は自前で行う必要がある。

単純なGet, PostのみのAPI通信しか行わないのであれば、ライブラリを使う必要はないと思いました。
MultipartとDownloadは頻発しないのであれば、URLSessionで対応できそうです。(DownloadはAlamofireでも悩みましたが。)
HTTP Status Codeによるエラー処理は一考の余地があると思ったので、後日考えてみたいと思います。