SwiftでMoyaを使う

Moyaを使用して簡単な参考ソースコードを作成します。
今回は下記URLのAPIを例として使用しました。
https://www.themoviedb.org/
人気映画、映画の詳細、検索などが利用できるAPIです。

# Moyaとは
Moyaはネットワーク抽象化レイヤーです。
MoyaはAlamofireを使用しており、ネットワークマネージャーを素早くセットアップできます。
必要な依存関係を追加することで、プロジェクトでMoyaを使用することができます。
プロジェクトをインストールする方法はいくつかあり、下記リンクにインストールに関する詳細が入手できます。
https://github.com/Moya/Moya#installation

# API
Moyaをインストールしたら、まずAPI.swiftファイルを作成し、さまざまなリクエストをもつ列挙型（enum）を作成します。
API列挙型をMoyaのTargetTypeで拡張し、必要な変数に準拠する必要があります。
baseURL, path, sampleData, taskやheaderなどです。

```
import Moya

enum API {
    case popular
    case movie(movieId: String)
    case search(query: String)
}

extension API: TargetType {
    var baseURL: URL {
        guard let url = URL(string: "https://api.themoviedb.org/3/") else { fatalError() }
        return url
    }
    
    var path: String {
        switch self {
        case .popular:
            return "movie/popular"
        case .movie(let movieId):
            return "movie/\(movieId)"
        case .search:
            return "search/movie"
        }
    }
    
    var method: Method {
        return .get
    }
    
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch self {
        case .popular, .movie:
            return .requestParameters(parameters: ["api_key": Constants.API.apiKey], encoding: URLEncoding.queryString)
        case .search(let query):
            return .requestParameters(parameters: ["query" : query, "api_key": Constants.API.apiKey], encoding: URLEncoding.queryString)
        }
    }
    
    var headers: [String : String]? {
        return nil
    }
}

```

baseURL:　API URLの共通接頭辞。
path: baseURLの後につく。場合によっては動的な値を置く必要がある。例えば、映画の詳細を取得するのであれば、映画IDを
>movie/{movieId}

のようなパスに配置する必要がある。
method: リクエストのHTTPメソッド。取得、投稿、削除、接続、ヘッド、オプション、パッチ、トレースが取得できる。
sampleData: テスト用の応答のスタブデータを作成することができます。テストを作成しない場合は、コードのように空のデータを返すことができます。
task: HTTPタスクを表します。いくつかのタスクがありますが、共通タスクについて説明します。クエリパラメータとしてapi_keyを渡す必要があるので、人気のある映画タイプにはリクエストパラメータを使用しました。

クエリパラメータやbodyを追加しないのであれば、requestPlainを使用します。
例として、クエリパラメータを追加するとき：　?api_key=xxx.
requestParametersを選ぶ必要があります。
また、エンコード可能なリクエストモデルを使用してbodyにrequestJSONEncodableを使用することができます。

ヘッダー：　ヘッダーはリクエストで使用されます。リクエストのためにヘッダーが必要な場合、辞書にヘッダーを追加します。

# ネットワークマネジャー
マッピングのための応答モデルを作る必要があります。
JSONレスポンスから応答モデルを生み出すために、下記URLを使いました。
https://app.quicktype.io
素早く応答モデルを生成するツールとしてこちらはおすすめです。
MovieResponse、MovieDetailResponse、SearchResponseのモデルを作成しました。
ファイル名はNetworkManager.swiftです。

```
import Moya

protocol Networkable {
    var provider: MoyaProvider<API> { get }

    func fetchPopularMovies(completion: @escaping (Result<MovieResponse, Error>) -> ())
    func fetchMovieDetail(movieId: String, completion: @escaping (Result<MovieDetailResponse, Error>) -> ())
    func fetchSearchResult(query: String, completion: @escaping (Result<SearchResponse, Error>) -> ())
}

class NetworkManager: Networkable {
    var provider = MoyaProvider<API>(plugins: [NetworkLoggerPlugin()])

    func fetchPopularMovies(completion: @escaping (Result<MovieResponse, Error>) -> ()) {
        request(target: .popular, completion: completion)
    }
    
    func fetchMovieDetail(movieId: String, completion: @escaping (Result<MovieDetailResponse, Error>) -> ()) {
        request(target: .movie(movieId: movieId), completion: completion)
    }
    
    func fetchSearchResult(query: String, completion: @escaping (Result<SearchResponse, Error>) -> ()) {
        request(target: .search(query: query), completion: completion)
    }
}

private extension NetworkManager {
    private func request<T: Decodable>(target: API, completion: @escaping (Result<T, Error>) -> ()) {
        provider.request(target) { result in
            switch result {
            case let .success(response):
                do {
                    let results = try JSONDecoder().decode(T.self, from: response.data)
                    completion(.success(results))
                } catch let error {
                    completion(.failure(error))
                }
            case let .failure(error):
                completion(.failure(error))
            }
        }
    }
}
```

フェッチメソッドとプロバイダーメソッドを含むプロトコルを作成します。
この例だと、メソッドは3つしかなく、APIファイルを備えたプロバイダーが機能します。
たくさんのリクエストを送信する必要がある場合、APIファイルは混乱してしまいます。
MovieAPI, LoginAPIのような異なるAPIファイルを作成でき、異なるプロバイダーを使うことができます。
したがって、それはもっと組織化されるでしょう。

MoyaはNetworkLoggerPluginとして呼ばれるプラグインがあり、要求と応答をコンソールに記録するために提供されます。
要求・応答の記録を見るためにこのプラグインを使用します。プラグインを使用してプロバイダーを初期化することができます。
フェッチメソッドの内部は応答モデルとクエリ以外は同じなので、汎用のリクエストメソッドを作成します。それはコードの重複を防ぎます。
Swift5に付属しているResult型を使用しました。
Result型は２つのケースを持つ列挙型として実装される：　success, failure
下記のように自身のエラー型を作成でき、エラーとして使用することができます。

```
enum NetworkError: Error {
    case notFound
    case unexpectedError
}
```

# 呼び出し

マネージャーを使うためにネットワークマネージャーからインスタンスを取得します。
テスト容易性の分類するためにネットワークマネージャーを注入するのが良いでしょう。
したがって、クラスをテストする必要がある際は、モックネットワークマネージャーを簡単に作成し、クラスに注入することができます。

```
private let networkManager: NetworkManager
init(networkManager: NetworkManager = NetworkManager()) {          
    self.networkManager = networkManager
}
```

もしテストを書く必要がなければ、下記のようにインスタンスを取得することができます。

```
private let networkManager = NetworkManager()
```

サービスから要求データをフェッチする準備をします。

```
func loadMovieDetail(movieId: String) {
    networkManager.fetchMovieDetail(movieId: movieId, completion: { [weak self] result in
        guard let strongSelf = self else { return }
        switch result {
        case .success(let movieDetailResponse):
            strongSelf.movieDetail = movieDetailResponse
        case .failure(let error):
            print(error.localizedDescription)
        }
    })
}
```

下記のリンクにサンプルプロジェクトがあります。
https://github.com/omerkolkanat/moya-example

プロジェクトのネットワークモジュールをどう素早くビルドできるか説明しています。お役に立てれば幸いです。

# 参考文献

https://medium.com/trendyol-tech/get-instantly-a-network-module-using-moya-in-swift-5f918ecdfb58