Help us understand the problem. What is going on with this article?

APIKitのRequestをProtocol用いてまとめてみた

More than 1 year has passed since last update.

はじめに

プロジェクトの中で初めて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にもあります。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away