最初に
前回の続きです。
こちらを見てからどーぞ。
https://qiita.com/daichi77/items/0c849108ada04d7083e0#
完成画面
こんな感じで僕のクラロワの最高トロフィーとかを表示できました。
Modelのファイルたち
解説
ClashRoyaleClientError.swift
エラーのパターンをまとめたenum
enum ClashRoyaleClientError: Error {
//通信に失敗
case connectionError(Error)
//レスポンスの解釈に失敗
case responseParseError(Error)
//エラーレスポンスを受け取った時
case apiError(Error)
}
ClashRoyaleAPIError
APIから帰ってきたエラーメッセージなどを格納するやつ
struct ClashRoyaleAPIError: Decodable, Error {
struct fieldError: Decodable {
let error: Bool
let status: Int
let message: String
}
}
Player.swift
APIから取ってきた情報を格納する構造体。
JSONにあわせて作りましょう。
Decodableを継承させておくといい感じにjsonを構造体に変換してくれます。
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)
になります。
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しか使いませんがとりあえず他のやつも定義しています。
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にしたりします。
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に変換したりしてます。
//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)
}
}
}
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を呼んでます。
記事が分かれてしまってすみません。
解説はコメントに書いてありますが聞きたいことがあればコメントください。
前回の記事