0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

毎日何か成果物か学んだことを書くAdvent Calendar 2019

Day 9

MVP意識しながら自分のクラロワアカウントをアプリに表示する。(Model編)

Posted at

最初に

前回の続きです。
こちらを見てからどーぞ。
https://qiita.com/daichi77/items/0c849108ada04d7083e0#

完成画面

image.png
こんな感じで僕のクラロワの最高トロフィーとかを表示できました。

Modelのファイルたち

スクリーンショット 2019-12-11 14.24.01.png

解説

ClashRoyaleClientError.swift
エラーのパターンをまとめたenum

ClashRoyaleClientError.swift
enum ClashRoyaleClientError: Error {
   
    //通信に失敗
    case connectionError(Error)
    
    //レスポンスの解釈に失敗
    case responseParseError(Error)
    
    //エラーレスポンスを受け取った時
    case apiError(Error)
}

ClashRoyaleAPIError
APIから帰ってきたエラーメッセージなどを格納するやつ

ClashRoyaleAPIError
struct ClashRoyaleAPIError: Decodable, Error {
    struct fieldError: Decodable {
        let error: Bool
        let status: Int
        let message: String
    }
}

Player.swift
APIから取ってきた情報を格納する構造体。
JSONにあわせて作りましょう。
Decodableを継承させておくといい感じにjsonを構造体に変換してくれます。

Player.swift
public struct Player: Decodable {
    let tag: String
    let name: String
    let expLevel: Int
    let trophies: Int
    let bestTrophies: Int
    let wins: Int
    let losses: Int
    let battleCount: Int
    let threeCrownWins: Int
    let challengeCardsWon: Int
    let challengeMaxWins: Int
    let tournamentCardsWon: Int
    let tournamentBattleCount: Int
    let donations: Int
    let donationsReceived: Int
    let totalDonations: Int
    let warDayWins: Int
    let clanCardsCollected: Int
    struct arena: Decodable {
        let id: Int
        let name: String
    }
    struct leagueStatistics: Decodable {
        struct currentSeason: Decodable {
            let trophies: Int
            let bestTrophies: Int
        }
        struct previousSeason: Decodable {
            let id: String
            let trophies: Int
            let bestTrophies: Int
        }
        struct bestSeason: Decodable {
            let id: String
            let trophies: Int
        }
    }
    struct Badges: Decodable {
        let name: String
        let level: Int?
        let maxLevel: Int?
        let progress: Int
    }
    let badges: [Badges]
    struct Achievements: Decodable {
        let name: String
        let stars: Int
        let value: Int
        let target: Int
        let info: String
    }
    let achievements: [Achievements]
    struct Cards: Decodable {
        let name: String
        let id: Int
        let level: Int
        let maxLevel: Int
        let count: Int
        struct iconUrls: Decodable {
            let medium: String
        }
    }
    let cards: [Cards]
    struct currentFavouriteCard: Decodable {
        let name: String
        let id: Int
        let maxLevel: Int
        struct iconUrls: Decodable {
            let medium: String
        }
    }
}

Result.swift
その名の通り結果を返すenum型です。
APIからデータが返ってくればsucceess(T)エラーが帰ってくればfailure(error)
になります。

Result.swift
enum Result<T, Error: Swift.Error> {
    case success(T)
    case failure(Error)
    
    init(value: T) {
        self = .success(value)
    }
    
    init(error: Error) {
        self = .failure(error)
    }
}

HTTPMethod.swift
今回はGETしか使いませんがとりあえず他のやつも定義しています。

HTTPMethod.swift
enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case put = "PUT"
    case head = "HEAD"
    case delete = "DELETE"
    case patch = "PATCH"
    case trace = "TRACE"
    case options = "OPTIONS"
    case connect = "CONNECT"
}

ClashRoyaleAPI.swift
ここでpathとかjsonを変換する型(今回はPlayerを取ってくるのでPlayerに)などを決めます。
例えばプレイヤーじゃなくクランを取ってくる場合はResponseの型をClan型にしたりします。
pathなども同様にPostならpostにしたりします。

ClashRoyaleAPI.swift
final class ClashRoyaleAPI {
    struct SearchUser: ClashRoyaleRequest {
        
        typealias Response = Player//ここは取ってくる情報によって違う値に変更
        
        let keyword: String
        var method: HTTPMethod {
            return .get
        }

        var path: String {
            return  "players" + keyword
        }
    }
}

ClashRoyaleRequest.swift
ここでリクエストの設定をしたりJsonをPlayer.swiftに変換したりしてます。

ClashRoyaleRequest.swift
//protocolで切り出してます。これをもとにリクエストを作ります。
protocol ClashRoyaleRequest {
    associatedtype Response: Decodable
    var baseURL: URL { get }
    var headers: [String: String] { get }
    var path: String { get }
    var method: HTTPMethod { get }
}
// baseURLとHeaderはどのリクエストでも共通なのでprotocolextensionで初期化
extension ClashRoyaleRequest {
    var baseURL: URL {
        return URL(string: "https://api.clashroyale.com/v1/")!
    }
    var headers: [String: String] {
        return [
            "Authorization": "Bearer {apikey}"//各自のAPIkey
        ]
    }
    
    func buildURLRequest() -> URLRequest {
        //baseURLとpathを合体。
        let url = baseURL.appendingPathComponent(path)
        let components = URLComponents(url: url, resolvingAgainstBaseURL: true)
        var urlRequest = URLRequest(url: url)
        urlRequest.url = components?.url
        urlRequest.httpMethod = method.rawValue//get
        urlRequest.allHTTPHeaderFields = headers//apikeyなど
        return urlRequest
    }
    //APIから返ってきた値が200から300ならjsonをUserモデルに変換してます。
    func response(from data: Data, urlResponse: URLResponse) throws -> Response {
        let decoder = JSONDecoder()
        if case (200..<300)? = (urlResponse as? HTTPURLResponse)?.statusCode {
            print(Response.self)
            //このResponse.selfはrequestによって型が変わります
            return try decoder.decode(Response.self, from: data)
        } else {
            //ClashRoyaleAPIErrorの型に変換
            throw try decoder.decode(ClashRoyaleAPIError.self, from: data)
        }
    }
}
ClashRoyaleClient.swift
class ClashRoyaleClient {
    private let session: URLSession = {
        let configuration = URLSessionConfiguration.default
        let session = URLSession(configuration: configuration)
        return session
    }()
    //api叩くところです。
    func send<Request: ClashRoyaleRequest> (
        request: Request,
        completion: @escaping (Result<Request.Response,ClashRoyaleClientError>) -> Void) {
        let urlRequest = request.buildURLRequest()
        let task = self.session.dataTask(with: urlRequest) {
            data, response, error in
            switch(data,response,error) {
            case (_,_, let error?):
                completion(Result(error: .connectionError(error)))
            case (let data?,let response?, _):
                do {//responseを読んでjsonを変換
                    let response = try request.response(from: data, urlResponse: response)

                    completion(Result(value: response))
                } catch let error as ClashRoyaleAPIError {//api呼べてないとか
                    completion(Result(error: .apiError(error)))
                } catch {//データは帰ってきたけどparseに失敗した時。
                    //jsonの形式とmodelの形式があっているか確認しましょう。
                    completion(Result(error: .responseParseError(error)))
                }
            default:
                fatalError("invalid response combination \(data),\(response), \(error)")
            }
        }
        DispatchQueue.main.async {
            task.resume()
        }
    }
}

まとめ

前回の記事に書いたPresenterからClashRoyaleClient.swiftを呼んでます。
記事が分かれてしまってすみません。
解説はコメントに書いてありますが聞きたいことがあればコメントください。

前回の記事

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?