はじめに
プロジェクトの中で初めてAPIKitを触ったので備忘録として書き残しておきます。
環境
- Swift 4.1.2
- APIKit 3.2.1
実装
1. Protocolの作成
MyRequest.swift
import Foundation
import APIKit
protocol MyRequest: Request { }
2. URLとHeaderを持つように拡張
MyRequest.swift
extension MyRequest {
var baseURL: URL {
return URL(string: BASE_URL)!
}
var headerFields: [String: String] {
guard let accessToken = MyAppLocalData.authToken() else {
return [:]
}
return ["X-Authentication-Token": accessToken]
}
}
MyAppLocalData.authToken()
はUserDefaults
を用いた以下のような実装。
MyAppLocalData.swift
enum LocalDataKey: String {
case authToken = "AuthToken"
}
class MyAppLocalData {
class func authToken() -> String? {
return UserDefaults.standard.string(forKey: LocalDataKey.authToken.rawValue)
}
class func setAuthToken(_ token: String?) {
if token == nil {return}
UserDefaults.standard.set(token!, forKey: LocalDataKey.authToken.rawValue)
UserDefaults.standard.synchronize()
}
}
3. Responseについて拡張
MyRequest.swift
extension MyRequest where Response: Decodable {
var dataParser: DataParser {
return DecodableDataParser()
}
func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response {
guard let data = object as? Data else {
throw ResponseError.unexpectedObject(object)
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if #available(iOS 10.0, *) {
decoder.dateDecodingStrategy = .iso8601
} else {
// Fallback on earlier versions
}
// Avoid decording error when "204 no content" etc
if data.count == 0 {
let emptyJson = "{}"
return try decoder.decode(Response.self, from: emptyJson.data(using: .utf8)!)
} else {
return try decoder.decode(Response.self, from: data)
}
}
}
DecodableDataParser()
は以下のようなクラス。
MyRequest.swift
final class DecodableDataParser: DataParser {
var contentType: String? {
return "application/json"
}
func parse(data: Data) throws -> Any {
return data
}
}
4. ResultをEnumで表現
MyRequest.swift
enum Result<T> {
case success(T)
case failure(Error)
}
使用例
今回は以下のような構造を持つUser
についての使用例を挙げます。
User.swift
struct User: Decodable {
let id: Int
let name: String
let email: String
let tel: String
}
また、新規作成や編集用に以下のような構造のEditingUser
も使用します。
EditingUser.swift
struct EditingUser: Decodable {
var id: Int?
var name: String?
var email: String?
var tel: String?
}
Repository
UserRepository.swift
import Foundation
import APIKit
struct UserRepository {
/// Userの登録
func post(user: EditingUser, handler: @escaping (Result<User>) -> Void) {
Session.send(UserPostRequest(user: user)) { result in
switch result {
case .success(let response):
handler(.success(response.data))
case .failure(let error):
handler(.failure(error))
}
}
}
/// Userの更新
func patch(user: EditingUser, handler: @escaping (Result<User>) -> Void) {
Session.send(UserPatchRequest(user: user)) { result in
switch result {
case .success(let response):
handler(.success(response.data))
case .failure(let error):
handler(.failure(error))
}
}
}
/// Userの削除
func delete(id: Int, _ handler: @escaping (Result<Any>) -> Void) {
Session.send(UserDeleteRequest(id: id)) { result in
switch result {
case .success(let response):
handler(.success(response))
case .failure(let error):
handler(.failure(error))
}
}
}
/// Userの取得
func get(id: Int, handler: @escaping (Result<User>) -> Void) {
Session.send(UserGetRequest(id: id)) { result in
switch result {
case .success(let response):
handler(.success(response.data))
case .failure(let error):
handler(.failure(error))
}
}
}
// MARK: - POST
// MyRequestプロトコルを使用!!
struct UserPostRequest: MyRequest {
typealias Response = UserPostResponse
let user: EditingUser
let method: HTTPMethod = .post
let path: String = "/users"
var bodyParameters: BodyParameters? {
var params: [MultipartFormDataBodyParameters.Part] = []
// required params
guard let name = user.name, let email = user.email else { return nil }
params.append(try! MultipartFormDataBodyParameters.Part(
value: name,
name: "name"))
params.append(try! MultipartFormDataBodyParameters.Part(
value: email,
name: "email"))
// optional params
if let tel = user.tel {
params.append(try! MultipartFormDataBodyParameters.Part(
value: tel,
name: "tel"))
}
return MultipartFormDataBodyParameters(parts: params)
}
}
struct UserPostResponse: Decodable {
let status: String
let data: User
}
// MARK: - PATCH
struct UserPatchRequest: MyRequest {
typealias Response = UserPatchResponse
let user: EditingUser
let method: HTTPMethod = .patch
var path: String {
guard let id = user.id else { return "" }
return "/users/\(id)"
}
var bodyParameters: BodyParameters? {
var params: [MultipartFormDataBodyParameters.Part] = []
// required params
guard let name = user.name, let email = user.email else { return nil }
params.append(try! MultipartFormDataBodyParameters.Part(
value: name,
name: "name"))
params.append(try! MultipartFormDataBodyParameters.Part(
value: email,
name: "email"))
// optional params
if let tel = user.tel {
params.append(try! MultipartFormDataBodyParameters.Part(
value: tel,
name: "tel"))
}
return MultipartFormDataBodyParameters(parts: params)
}
}
struct UserPatchResponse: Decodable {
let status: String
let data: User
}
// MARK: - DELETE
struct UserDeleteRequest: MyRequest {
typealias Response = UserDeleteResponse
let id: Int
let method: HTTPMethod = .delete
var path: String {
return "/users/\(id)"
}
}
struct UserDeleteResponse: Decodable {
let status: String?
}
// MARK: - GET
struct UserGetRequest: MyRequest {
typealias Response = UserGetResponse
let id: Int
let method: HTTPMethod = .get
var path: String {
return "/users/\(id)"
}
}
struct UserGetResponse: Decodable {
let status: String
let data: User
}
}
Repositoryの使用例
getの場合のみ書いておきます。他の場合も同様に使用できます。
let repository = EventRepository()
repository.get(id: self.id) { [unowned self] result in
switch result {
case .success(let user):
self.user = user
case .failure(let error):
print(error)
}
}
おわりに
少し長くなってしまいましたが、APIKitを用いる時はこれを元に実装していこうかなあと思います。
もっと良い書き方あれば編集リクエストガンガン送ってください。
コードはGitHubGistにもあります。