LoginSignup
2
2

More than 1 year has passed since last update.

URLSessionを使ってAPIクライアントを実装する

Posted at

概要

APIクライアント実装のメモです。

環境

Xcode 13.1
以下の内容はXcodeのPlayground環境を使っています。

リクエストするためのprotocolを実装する

GET・POSTリクエストをenumで定義します。
APIのURL, パス, HTTPメソッド, レスポンスを表現するプロトコルを実装します。

ApiSessionPlayground
enum HttpMethod: String {
    case `get` = "GET"
    case post = "POST"
}

protocol Requestable {
    associatedtype Response: Decodable
    var baseURL: URL { get }
    var path: String { get }
    var httpMethod: HttpMethod { get }
}

レスポンスをResult<T>で表現する

APIレスポンスの結果は成功か失敗かで表現します。
enumを定義します。

ApiSessionPlayground
enum Result<T> {
    case success(T)
    case failure(Error)
}

APIリクエストを実行するクラスを定義する

Sessionというクラスを定義しました。
API通信が終わった後にコールバックを実行します。
コールバックの引数には先ほど定義したResult<T>が入る想定です。

ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ request: T, completion: @escaping (Result<T.Response>) -> ()) {
    }
}

URLRequestを作成する

APIのURL, パス, HTTPメソッドからURLRequestを作成します。

ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue
    }
}

URLSessionDataTaskを準備する

先程のURLRequestからタスクを作成し実行します。

ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue

        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
        }

        task.resume()
    }
}

API通信時に発生するエラーを定義する

エラーをよりきめ細かく検知するためにカスタムエラーを定義します。

ApisessionPlayground
enum SessionError: Error {
    case noResponse
    case unacceptableStatuCode(Int)
    case noData
}

APIから受け取ったデータをハンドリングする

以下のことを行なっています。
1. レスポンスを HTTPURLResponseに変換する
2. ステータスコードを確認する
3. データをデコードする

ApiSessionPlayground
final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue

        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let response = response as? HTTPURLResponse else {
                completion(.failure(SessionError.noResponse))
                return
            }

            guard 200..<300 ~= response.statusCode else {
                completion(.failure(SessionError.unacceptableStatuCode(response.statusCode)))
                return
            }

            guard let data = data else {
                completion(.failure(SessionError.noData))
                return
            }

            do {
                let objects = try JSONDecoder().decode(T.Response.self, from: data)
                completion(.success(objects))
            } catch {
                completion(.failure(error))
            }
        }

        task.resume()
    }
}

QiitaのAPIを叩いてみる

QiitaAPIを叩くための構造体を定義します。
また、Qiitaの投稿を表現する構造体を定義します。

ApiSessionPlayground
struct Article: Codable {
    let title: String
}

struct ArticlesApiRequestable: Requestable {
    typealias Response = [Article]
    var baseURL: URL = URL(string: "https://qiita.com")!
    var path: String = "/apiv2/items"
    var httpMethod: HttpMethod = .get
}

APIリクエストを実行する。

getArticles()という関数を定義して、APIをコールします。

ApiSessionPlayground
func getArticles() {
    let requestable = ArticlesApiRequestable()
    let session = Session()
    session.send(requestable) { result in
        switch result {
        case .success(let articles):
            print(articles.count)
        case .failure(let error):
            print(error)
        }
    }
}

getArticles()

全体のコードは以下のようになります。

ApiSessionPlayground
import Foundation

enum SessionError: Error {
    case noResponse
    case unacceptableStatuCode(Int)
    case noData
}

enum Result<T> {
    case success(T)
    case failure(Error)
}

enum HttpMethod: String {
    case `get` = "GET"
    case post = "POST"
}

protocol Requestable {
    associatedtype Response: Decodable
    var baseURL: URL { get }
    var path: String { get }
    var httpMethod: HttpMethod { get }
}

final class Session {
    func send<T: Requestable>(_ requestable: T, completion: @escaping (Result<T.Response>) -> ()) {
        let url = requestable.baseURL.appendingPathComponent(requestable.path)
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = requestable.httpMethod.rawValue

        let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let response = response as? HTTPURLResponse else {
                completion(.failure(SessionError.noResponse))
                return
            }

            guard 200..<300 ~= response.statusCode else {
                completion(.failure(SessionError.unacceptableStatuCode(response.statusCode)))
                return
            }

            guard let data = data else {
                completion(.failure(SessionError.noData))
                return
            }

            do {
                let objects = try JSONDecoder().decode(T.Response.self, from: data)
                completion(.success(objects))
            } catch {
                completion(.failure(error))
            }
        }

        task.resume()
    }
}

struct Article: Codable {
    let title: String
}

struct ArticlesApiRequestable: Requestable {
    typealias Response = [Article]
    var baseURL: URL = URL(string: "https://qiita.com")!
    var path: String = "/api/v2/items"
    var httpMethod: HttpMethod = .get
}

func getArticles() {
    let requestable = ArticlesApiRequestable()
    let session = Session()
    session.send(requestable) { result in
        switch result {
        case .success(let articles):
            print(articles.count)
        case .failure(let error):
            print(error)
        }
    }
}

getArticles()

参考リンク

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2