SplatNet2のライブラリを更新している

Swift

SplatNet2ライブラリ

tkgstrator/SplatNet2
Contribute to tkgstrator/SplatNet2 development by creating an account on GitHub.

Swiftで簡単にAPIを叩けるライブラリ、のつもりで作成したのだがあまりにもゴミコードだったので泣いています。

まあ簡単にいえばiksm_sessionをとってきたり更新したり、サーモンラン用のリザルトをとってきたりとできるコードだったのですがあまりに酷いので書き直すことにしました。

iOS13以降にはCombineという面白い仕組みがあるのでこれを利用すればクロージャの数を減らしつつ良いコードが書けそうな気がします。

Salmonia3は以下の参考記事を利用させていただいてRealmにデータを書き込む際にCodableを使って一気に変換しているのですが、よく考えたらAPIのレスポンスをライブラリが上手く整形してやればこんな処理は不要なわけです。

【Swift4】Realm+Codableを使ったお手軽なDB Part.1(モデル編) - Qiita
はじめに みなさん、こんにちは。株式会社NexceedでiOSエンジニアをしている@cottpanです😎 アプリの開発を行う中で、データベースの操作は避けて通れない操作だと思います。 APIを叩いてJSONをパース パースし...

つまり、何らかのクラスや構造体を返してしまえばいちいちキーなんて使わなくてもメンバ変数を使ってパパっと値をとってこれるわけです。

ライブラリからエラーを起こさずに値が返ってきている時点でちゃんとデータが入っていることは間違いなく、(返り値に対する)バリデーションも不要になります。これはなんか高便利そうですね?

Combine + Alamofire

というわけで以下の記事を参考にCombineを使ってタスクを渡してそれをクロージャで処理できるライブラリをつくることにしました。

Combine+Alamofire+SwiftUIでAPI実行 - Qiita
CombineとAlamofireを組み合わせてSwiftUIを実行してみたい。 2019年に発表されたSwiftUIとCombineのフレームワークですが、まだUIKitなどの長年使われてきたもの比べるとまだまだ参考になる記事...

クロージャを使う仕組みは@escapingを使うのと対して変わらないのですが、APIを叩く際のプロトコルを決めておくことで新しいエンドポイントがでたときにも柔軟に対応することができます。

protocol APIProtocol {
    associatedtype ResponseType: Decodable
    
    var method: HTTPMethod { get }
    var baseURL: URL { get }
    var path: String { get set }
    var headers: [String: String]? { get }
    var allowConstrainedNetworkAccess: Bool { get }
}

extension APIProtocol {
    var baseURL: URL {
        return URL(string: "https://app.splatoon2.nintendo.net/api/")!
    }
    
    var headers: [String: String]? {
        return nil
    }
    
    var allowConstrainedNetworkAccess: Bool {
        return true
    }
}

そしてAPIプロトコルを継承したリクエストプロトコルを作ります

protocol RequestProtocol: APIProtocol, URLRequestConvertible {
    var parameters: Parameters? { get }
    var encoding: JSONEncoding { get }
}

extension RequestProtocol {
    var encoding: JSONEncoding {
        return JSONEncoding.default
    }
    
    public func asURLRequest() throws -> URLRequest {
        var request = URLRequest(url: baseURL.appendingPathComponent(path))
        request.httpMethod = method.rawValue
        request.allHTTPHeaderFields = headers
        request.timeoutInterval = TimeInterval(5)
        request.allowsConstrainedNetworkAccess = allowConstrainedNetworkAccess
        
        if let params = parameters {
            request = try encoding.encode(request, with: params)
        }
        
        return request
    }
}

ここではAlamofireの構造体が良かったのでそのまま利用したとのこと。なのでimport Alamofireを忘れないようにしましょう。

参考記事ではURLEncodingを採用していますが、SplatNet2はほぼすべてのリクエストでJSONEncodingしかつかわないので問題ないでしょう。唯一の例外がs2s APIなのですがそれはそれでまた別の話。

なのでEncodingとして型はJSONEncodingではなくてEncodingのようなものを持ちたかったのですが、それがなかったので少し別の方法を考えなくてはいけません。

asURLRequest()でURLRequestを作成してそれをAlamofireで実行するという仕組みです。

import Foundation
import Combine
import Alamofire
import SwiftyJSON

struct NetworkPublisher {
    
    private static let contentType = ["application/json"]
    private static let retryCount = 1
    static let decoder: JSONDecoder()
    
    static func publish<T: RequestProtocol, V: Decodable>(_ request: T) -> Future<V, APIError> where T.ResponseType == V {
        return Future { promise in
            let alamofire = AF.request(request)
                .validate(statusCode: 200...300)
                .validate(contentType: contentType)
                .cURLDescription { request in
                    print(request)
                }
                .responseJSON { response in
                    switch response.result {
                    case .success(let value):
                        do {
                            let json = try JSON(value).rawData()
                            let data = try decoder.decode(V.self, from: json)
                            print(data)
                            promise(.success(data))
                        } catch(let error) {
                            print(error)
                            promise(.failure(APIError.invalid))
                        }
                    case .failure(let error):
                        print(error)
                        promise(.failure(APIError.failure))
                    }
                }
            alamofire.resume()
        }
    }
}

public enum APIError: Error {
    case failure
    case invalid
    case requests
    case unavailable
    case upgrade
    case unknown
    case badrequests
}

今回は意味もなく(おい)SwiftyJSONを導入しているのでJSONDecoderのところの記述が少し異なります。

まあ多分気にしなくても大丈夫。

進捗情報

とりあえずリザルトのIDをを指定すれば取得できるようにはなりました。

SplatNet2のバグなのかは知らないのですが、イベントなしのWAVEのキーがwater-levelsとかいう謎な値になっています。まあひょっとしたら-と返すのがダサくてそうしたのかもしれません。

WaveもEventもEnumでそれぞれ値があるのですが、このまま文字列で返したほうがいいのかどうかは考えどころですね。

いまはSwift風にLCCで変数名を設定していて、ネストもSplatNet2準拠なのですが時刻のデータなどは普通にネストに入れてしまってもいいような気がします(startTime, endTime, playTime)の三つが並んでいるのが若干違和感。

で、ここまで書いておいてステージIDが取れていないことに気付いたのですが、今日中に頑張って直したいと思います。

コメント

タイトルとURLをコピーしました