SplatNet2ライブラリ
Swiftで簡単にAPIを叩けるライブラリ、のつもりで作成したのだがあまりにもゴミコードだったので泣いています。
まあ簡単にいえばiksm_sessionをとってきたり更新したり、サーモンラン用のリザルトをとってきたりとできるコードだったのですがあまりに酷いので書き直すことにしました。
iOS13以降にはCombineという面白い仕組みがあるのでこれを利用すればクロージャの数を減らしつつ良いコードが書けそうな気がします。
Salmonia3は以下の参考記事を利用させていただいてRealmにデータを書き込む際にCodableを使って一気に変換しているのですが、よく考えたらAPIのレスポンスをライブラリが上手く整形してやればこんな処理は不要なわけです。

つまり、何らかのクラスや構造体を返してしまえばいちいちキーなんて使わなくてもメンバ変数を使ってパパっと値をとってこれるわけです。
ライブラリからエラーを起こさずに値が返ってきている時点でちゃんとデータが入っていることは間違いなく、(返り値に対する)バリデーションも不要になります。これはなんか高便利そうですね?
Combine + Alamofire
というわけで以下の記事を参考にCombineを使ってタスクを渡してそれをクロージャで処理できるライブラリをつくることにしました。

クロージャを使う仕組みは@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が取れていないことに気付いたのですが、今日中に頑張って直したいと思います。
自身を天才と信じて疑わないマッドサイエンティスト。二つ上の姉は大英図書館特殊工作部勤務、額の十字架の疵は彼女につけられた。
コメント