普通にURLSessionとCombineでURLSession

# 概要
URLSessionの通常の書き方、Combineでの書き方について調べました。

# 題材
QiitaのLGTMの取得APIを例に利用します。
参考: [Qiita APIで自分の記事のLGTMとViewを取得する。 - Qiita](https://qiita.com/Sho-heikun/items/6d5b938d1ea4adb89a08)

現在LGTMが5つの[記事](https://qiita.com/Sho-heikun/items/a9ead7285d10aadf5643)のLGTMについて取得します。

```
https://qiita.com/api/v2/items/a9ead7285d10aadf5643/likes
```

このURLをGETで叩けば取得可能です。



# 普通のURLSession

```swift
import Foundation

let qiitaURL = URL(string: "https://qiita.com/api/v2/items/a9ead7285d10aadf5643/likes")!

struct LGTM: Codable {
    var created_at: String
    var user: User
}
struct User: Codable {
    var id: String
}

let session = URLSession.shared.dataTask(with: qiitaURL) {data, response, error in
    if let error = error {
        fatalError("Error: \(error.localizedDescription)")
    }
    guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
        fatalError("Error: invalid HTTP response code")
    }
    guard let data = data else {
        fatalError("Error: missing response data")
    }

    do {
        let lgtms = try JSONDecoder().decode([LGTM].self, from: data)
        lgtms.forEach{ lgtm in
            print("username: \(lgtm.user.id), created_at \(lgtm.created_at)")
        }
    }
    catch {
        print("Error: \(error.localizedDescription)")
    }
}
session.resume()
```
`let session`の型は`URLSessionDataTask`です。
`URLSessionDataTask`のresumeを呼び出すことで通信します。

URLSessionのメソッドdataTaskの定義は以下です。

```swift

open func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
```


# CombineでURLSession
Combineで記述すると以下です。

```swift
import Foundation
import Combine

var cancellable = URLSession.shared
    .dataTaskPublisher(for: qiitaURL)
    .tryMap { element -> Data in
        guard let httpResponse = element.response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        return element.data
    }
    .decode(type: [LGTM].self, decoder: JSONDecoder())
    .sink(receiveCompletion: {
        print("complete! \($0)")
    }, receiveValue: { lgtms in
        lgtms.forEach{ lgtm in
            print("username: \(lgtm.user.id), created_at \(lgtm.created_at)")
        }
    })
```

少し処理が変わってしまってますが、Combineの方がcompletionHandlerでの処理が少なくなり、見通しがよくなるように感じます。

# Tips
## JsonDecoderであるのかないのかわからない名前を指定する。
Jsonのフィールド名にないものを指定してしまうと、
`Error: The data couldn’t be read because it is missing.`とJsonDecoderがエラーを出します。

例えば次のようにstructを定義した場合です。

```swift
struct User: Codable {
    var id: String
    var aaa: String
}
```

こういう時は、ないかもしれないフィールド名をOptionalにすることで`nil`が入り、エラーを防ぐことが可能です。

```swift
struct User: Codable {
    var id: String
    var aaa: String?
}
```


## URLSession.sharedとは？
URLSession.sharedはサクッと書きたいときに使うsingletonです。
デフォルトの挙動で動くため、高度な処理ができません。

* サーバーからの逐次のデータ取得はできない
* 大きくカスタマイズできない
* 認証関係に制限あり
* アプリが動いていないときにダウンロードやアップロードできない


[shared | Apple Developer Documentation](https://developer.apple.com/documentation/foundation/urlsession/1409000-shared)


## element -> Data とは何をしている？
クロージャの引数を明示しているだけです。
`-> Data`の部分がなくても動きます。

[Closures — The Swift Programming Language (Swift 5.3)](https://docs.swift.org/swift-book/LanguageGuide/Closures.html#ID95)


## 配列の要素をひとつ１つ処理したい
CombineでURLSessionのsubscribe処理は、現在forEachによる入れ子構造となっています。

```swift
.decode(type: [LGTM].self, decoder: JSONDecoder())
.sink(receiveValue: { lgtms in
        lgtms.forEach{ lgtm in
            print("username: \(lgtm.user.id), created_at \(lgtm.created_at)")
        }
    }
```

こちらの入れ子をなくすには、`flatMap`が利用できます。

```swift
.decode(type: [LGTM].self, decoder: JSONDecoder())
.flatMap {$0.publisher}
.sink(receiveCompletion: {
    print("complete! \($0)")
}, receiveValue: { lgtm in
    print("username: \(lgtm.user.id), created_at \(lgtm.created_at)")
})
```

# 終わり