概要
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から受け取ったデータをハンドリングする
以下のことを行なっています。
- レスポンスを
HTTPURLResponse
に変換する - ステータスコードを確認する
- データをデコードする
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()
参考リンク