Alamofire + Combine を試す

## はじめに
最近、 iOS開発、SwiftUI の勉強をしているのですが、WebAPI の呼び出しをするため Alamofire を選定しました。
非同期処理では Alamofire ＋ RxSwift などを使用するのが一般的のようですが、[Alamofire 5.2.0](https://github.com/Alamofire/Alamofire/releases/tag/5.2.0) で Apple 製の非同期ライブラリ Combine がサポートされましたので、 Alamofire + Combine を試してみました。

## ライブラリ等
Xcode : 12.4
Alamofire : 5.4.1

## サンプルアプリ
サンプルアプリとして、Chatwork API トークンを TextField に入力させ、値が取得できたら name に名前をセットして表示する、ミニマムなアプリを作成しました。

WebAPI は [Chatwork API](https://developer.chatwork.com/ja/) の [/me](https://developer.chatwork.com/ja/endpoint_me.html) を使用しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24356/ff3eb1eb-bba7-ccd9-bd06-3886cca4cbc0.png)

## 実装

```swift:LoginViewModel.swift
import Foundation
import Combine
import Alamofire

class LoginViewModel: ObservableObject {
    private var disposeBag = Set<AnyCancellable>()

    @Published var token = ""
    @Published var name = "name"

    func getMe() {
        let decoder = JSONDecoder()
        decoder.keyDecodingStrategy = .convertFromSnakeCase

        AF.request("https://api.chatwork.com/v2/me", headers: ["X-ChatWorkToken": token])
            .publishDecodable(type: MeModel.self, decoder: decoder)
            .sink { data in
                guard let me = data.value else {
                    print("error")
                    return
                }
                print(me.name)
                print(me.accountId)
                self.name = me.name
            }.store(in: &disposeBag)
    }

}

```

`.publishDecodable(type: MeModel.self, decoder: decoder)`
が DataResponsePublisher を返却するため、Combine で扱うことができるようになります。

Chatwork API のレスポンスは、 key がスネークケース`account_id`ですが、Swift ではキャメルケース`accountId`ですので、
`decoder.keyDecodingStrategy = .convertFromSnakeCase`
で、変換するように指定しています。

## Decodableを実装したデータモデルの定義
自分自身の情報を表すデータモデルは以下のように定義しました。

<details><summary>データの定義 MeModel.swift</summary><div>

```swift:MeModel.swift
import Foundation

struct MeModel: Decodable {
    var accountId: Int
    var roomId: Int
    var name: String
    var chatworkId: String
    var organizationId: Int
    var organizationName: String
    var department: String
    var title: String
    var url: String
    var introduction: String
    var mail: String
    var telOrganization: String
    var telExtension: String
    var telMobile: String
    var skype: String
    var facebook: String
    var twitter: String
    var avatarImageUrl: String
    var loginMail: String
}
```
</div></details>

参考までに、UIの定義はこんな感じです。

<details><summary>UIの定義 ContentView.swift</summary><div>

```swift:MeModel.swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = LoginViewModel()

    var body: some View {
        NavigationView {
            VStack(alignment: .center) {
                VStack(alignment: .leading) {
                    Text("Token")
                    TextField("token", text: $viewModel.token, onCommit: {
                    })
                        .keyboardType(.alphabet)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .padding(.horizontal)

                    Text(viewModel.name)

                }
                    .padding(10)

                Divider()

                Button("Get") {
                    viewModel.getMe()
                }
                    .padding()
            }
                .overlay(RoundedRectangle(cornerRadius: 5).stroke())
                .padding(.horizontal, 50)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```
</div></details>

## Moya は Combine に対応していない

ちなみに、Alamofire をより簡単に利用するための [Moya](https://github.com/Moya/Moya) というライブラリがありますが、14.0.0 では Combine にはまだ対応していません。
14.0.0 beta で一度導入されたものの、[問題があってリリース前に消えたようです](https://github.com/Moya/Moya/blob/f5366c0f1d9502de3f50555cb7be5da36740edec/Changelog.md#1400-beta4---2019-10-05)。