Moyaを使ってApiクライアント作成する

github.com

環境

  • Swift 4.1.2
  • Moya 11.0.2

Carthageでインストール

// Cartfile
github "Moya/Moya"
carthage update --platform iOS

ビルドが終わったらプロジェクトに追加します f:id:y-hryk:20180810185316p:plain

使い方

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))
            }
        }
    }
}