Moyaを使ってApiクライアント作成する
環境
- Swift 4.1.2
- Moya 11.0.2
Carthageでインストール
// Cartfile github "Moya/Moya"
carthage update --platform iOS
ビルドが終わったらプロジェクトに追加します
使い方
GithubのApiを使って以下の情報を取得してみます
- ユーザー情報 (/users/:username")
- ユーザーのリポジトリ情報 (/users/:username/repos")
モデルを作成
Codableを使用します。
※全部は大変なのでそれっぽいフィールドだけパースします。
// UserProfile.swift // ユーザー情報 struct UserProfile: Codable { let id: Int let name: String let email: String? let avatarUrl: String private enum CodingKeys: String, CodingKey { case id case name case email case avatarUrl = "avatar_url" } } // Repository.swift // ユーザーのリポジトリ情報 struct Repository: Codable { let id: Int let name: String let url: String private enum CodingKeys: String, CodingKey { case id case name case url = "html_url" } }
リクエストを作成
enumにエンドポイントを定義。TargetType
に準拠してリクエスト内容を記述します。
// GitHubAPI.swift enum GitHub { case profile(name: String) case repository(name: String, type: String) } extension GitHub: TargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var path: String { switch self { case let .profile(name): return "/users/\((name))" case let.repository(name, _): return "/users/\(name)/repos" } } var method: Moya.Method { return .get } var sampleData: Data { var path = "" switch self { case .profile: path = Bundle.main.path(forResource: "profile", ofType: "json")! case .repository: path = Bundle.main.path(forResource: "repository", ofType: "json")! } return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { switch self { case .profile: return .requestPlain case let .repository(_, type): return .requestParameters(parameters: ["type" : type], encoding: URLEncoding.default) } } var headers: [String : String]? { return ["Content-Type": "application/json"] } }
リクエスト
// ユーザー情報 let provider = MoyaProvider<GitHub>() provider.request(.profile(name: "y-hryk")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let profile = try! decoder.decode(UserProfile.self, from: response.data) print(profile) case let .failure(error): print(error) break } } // ユーザーのリポジトリ情報 let provider = MoyaProvider<GitHub>() provider.request(.repository(name: "y-hryk", type: "all")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let data = try! decoder.decode([Repository].self, from: response.data) print(data) case let .failure(error): print(error) break } }
使い方2
使い方1
の例だとAPIが増えるたびにリクエストの処理に分岐が増えていきます。
なのでenumを使用しない記述方法もみていきます。
TargetTypeに準拠したプロトコルを定義
TargetType
に準拠したプロトコルを定義して共通となる処理を記述します
// GitHubAPIService.swift class GitHubAPIService {} protocol GitHubAPITargetType: TargetType { } extension GitHubAPITargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var headers: [String : String]? { return ["Content-Type": "application/json"] } }
リクエストを作成
拡張したプロトコルGitHubAPITargetType
に準拠してリクエストを作成していきます。
// UserProfileRequest.swift extension GitHubAPIService { struct UserProfileRequest: GitHubAPITargetType { var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)" } var task: Task { return .requestPlain } var sampleData: Data { return Data() } let name: String init(name: String) { self.name = name } } }
// RepositoryRequest.swift extension GitHubAPIService { struct RepositoryRequest: GitHubAPITargetType { var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)/repos" } var sampleData: Data { let path = Bundle.main.path(forResource: "repository", ofType: "json")! return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { return .requestParameters(parameters: ["type" : self.type], encoding: URLEncoding.default) } let name: String let type: String init(name: String, type: String) { self.name = name self.type = type } } }
リクエスト
enumを使用した方法とほとんど変わらずリクエストできます。
// ユーザー情報 let provider = MoyaProvider<GitHubAPIService.UserProfileRequest>() provider.request(GitHubAPIService.UserProfileRequest(name: "y-hryk")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let data = try! decoder.decode(UserProfile.self, from: response.data) print(data) case let .failure(error): print(error) } } // ユーザーのリポジトリ情報 let provider = MoyaProvider<GitHubAPIService.RepositoryRequest>() provider.request(GitHubAPIService.RepositoryRequest(name: "y-hryk", type: "all")) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() let data = try! decoder.decode([Repository].self, from: response.data) print(data) case let .failure(error): print(error) } }
MoyaProviderの生成 + パース処理を共通化
MoyaProviderの生成とパース部分を隠蔽してシンプルな呼び出しに変更していきます
参考
- Moya/MultiTarget.md at master · Moya/Moya · GitHub
以下みたいに呼び出せるように修正します
GitHubAPIService().send(GitHubAPIService.UserProfileRequest(name: "y-hryk")) { (result) in switch result { case let .success(response): print(response) case let .failure(error): print(error) } }
TargetTypeに準拠したプロトコルを修正
// GitHubAPIService.swift protocol GitHubAPITargetType: TargetType { // レスポンスの型を定義できるように変更 // 追加 associatedtype Response: Codable } extension GitHubAPITargetType { var baseURL: URL { return URL(string: "https://api.github.com")! } var headers: [String : String]? { return ["Content-Type": "application/json"] } }
モデルを修正
// UserProfileRequest.swift extension GitHubAPIService { struct UserProfileRequest: GitHubAPITargetType { // 追加 typealias Response = UserProfile var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)" } var sampleData: Data { let path = Bundle.main.path(forResource: "profile", ofType: "json")! return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { return .requestPlain } let name: String init(name: String) { self.name = name } } } // RepositoryRequest.swift extension GitHubAPIService { struct RepositoryRequest: GitHubAPITargetType { // 追加 typealias Response = [Repository] var method: Moya.Method { return .get } var path: String { return "/users/\(self.name)/repos" } var sampleData: Data { let path = Bundle.main.path(forResource: "repository", ofType: "json")! return FileHandle(forReadingAtPath: path)!.readDataToEndOfFile() } var task: Task { return .requestParameters(parameters: ["type" : self.type], encoding: URLEncoding.default) } let name: String let type: String init(name: String, type: String) { self.name = name self.type = type } } }
リクエスト処理をラップする
これで最初に示したような呼び出し方が可能になります
// GitHubAPIService.swift protocol GitHubAPI { func send<T: GitHubAPITargetType>(_ request: T, completion: @escaping (Result<T.Response, Moya.MoyaError>) -> Void) } class GitHubAPIService: GitHubAPI { func send<T>(_ request: T, completion: @escaping (Result<T.Response, MoyaError>) -> Void) where T : GitHubAPITargetType { let provider = MoyaProvider<T>() provider.request(request) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() if let model = try? decoder.decode(T.Response.self, from: response.data) { completion(.success(model)) print(try! response.mapJSON()) } else { completion(.failure(.jsonMapping(response))) } case let .failure(error): completion(.failure(error)) } } } }
スタブを利用する
GitHubAPIService
,GitHubAPIStub
共にGitHubAPI
に準拠しているので
DIを用いてスタブに差し替えられる。
// GitHubAPIStub.swift class GitHubAPIStub: GitHubAPI { func send<T>(_ request: T, completion: @escaping (Result<T.Response, MoyaError>) -> Void) where T : GitHubAPITargetType { // スタブを返すように設定。このすることでリクエストに定義した`sampleData`が替えるようになります。 let provider = MoyaProvider<T>(stubClosure: MoyaProvider.immediatelyStub) provider.request(request) { (result) in switch result { case let .success(response): let decoder = JSONDecoder() if let model = try? decoder.decode(T.Response.self, from: response.data) { completion(.success(model)) } else { completion(.failure(.jsonMapping(response))) } case let .failure(error): completion(.failure(error)) } } } }