概要
Swift5.5から利用できるasync/awaitを使い通信クラスを書きました。
Qiitaの記事一覧を取得するAPIを使います。
仕様ではエラーレスポンスが
{
"message": "Not found",
"type": "not_found"
}
の形式で返されるのでこちらに対応します。
APIConfigure.swift
import Foundation
enum APIError: Error {
case server(Int)
case decode(Error)
case errorResponse(Error)
case noResponse
}
enum HttpMethod: String {
case get = "GET"
case post = "POST"
}
protocol Requestable {
associatedtype SuccessModel
associatedtype FailureModel: Error
var url: String { get }
var httpMethod: HttpMethod { get }
var headers: [String: String] { get }
var parameters : [String: String] { get }
func successDecode(from data: Data) throws -> SuccessModel
func failureDecode(from data: Data) throws -> FailureModel
}
extension Requestable {
var urlRequest: URLRequest {
var components = URLComponents(string: url)!
components.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
let componentsUrl = components.url!
var request = URLRequest(url: componentsUrl)
request.httpMethod = httpMethod.rawValue
headers.forEach { key, value in
request.addValue(value, forHTTPHeaderField: key)
}
return request
}
}
class APIConfigure {
static func request<T: Requestable>(_ requestable: T) async throws -> T.SuccessModel {
let request = requestable.urlRequest
let (data, response) = try await URLSession.shared.data(for: request, delegate: nil)
guard let response = response as? HTTPURLResponse else {
throw APIError.noResponse
}
if case 200..<300 = response.statusCode {
do {
let model = try requestable.successDecode(from: data)
return model
} catch let decodeError as DecodingError {
throw APIError.decode(decodeError)
}
} else if response.statusCode >= 500 {
throw APIError.server(response.statusCode)
} else {
do {
let errorModel = try requestable.failureDecode(from: data)
throw APIError.errorResponse(errorModel)
} catch let decodeError as DecodingError {
throw APIError.decode(decodeError)
}
}
}
}
APIRequest.swift
import Foundation
struct APIRequest {
/// 記事一覧取得
struct QiitaArticleItemsAPIRequest: Requestable {
typealias SuccessModel = [QiitaArticleItem]
typealias FailureModel = QiitaError
var url = "https://qiita.com/api/v2/items"
var httpMethod = HttpMethod.get
var headers: [String: String] = [:]
var parameters: [String: String] = [:]
func successDecode(from data: Data) throws -> [QiitaArticleItem] {
let decoder = JSONDecoder()
return try decoder.decode([QiitaArticleItem].self, from: data)
}
func failureDecode(from data: Data) throws -> QiitaError {
let decoder = JSONDecoder()
return try decoder.decode(QiitaError.self, from: data)
}
}
}
QiitaArticleItem.swift
import Foundation
struct QiitaArticleItem: Codable {
let title: String
let url: String
let user: QiitaUser
}
struct QiitaUser: Codable {
let profile_image_url: String
}
struct QiitaError: Codable, Error {
let message: String
let type: String
}
APIClient.swift
import Foundation
class APIClient {
/// Qiitaの記事取得
func getQiitaArticleItems(page: Int, perPage: Int) async throws -> [QiitaArticleItem] {
var request = APIRequest.QiitaArticleItemsAPIRequest()
request.parameters = ["page": "\(page)",
"per_page": "\(perPage)"]
return try await APIConfigure.request(request)
}
}
ViewControllerなどで通信する場合
Task {
do {
let articles = try await APIClient().getQiitaArticleItems(page: 1, perPage: page)
self.articles = articles
// UITableViewなど使っている場合はデータ更新
// self.tableView.reloadData()
} catch {
// エラー処理
if let apiError = error as? APIError {
switch apiError {
case .server(let statusCode):
print("statusCode=\(statusCode)")
case .decode(let error):
print(error.localizedDescription)
case .noResponse:
print("No Response")
case .errorResponse(let error):
if let error = error as? QiitaError {
let alert = UIAlertController(title: "error", message: "Message=\(error.message)\nType=\(error.type)\n", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default) {_ in
self.dismiss(animated: true, completion: nil)
}
alert.addAction(ok)
present(alert, animated: true, completion: nil)
} else {
print(error.localizedDescription)
}
}
} else {
print(error.localizedDescription)
}
}
}
悩んだポイントorわからないこと
-
成功時、エラー時でレスポンスのjsonの構造が違うため
successDecode()
,failureDecode()
と分けたがなんか気持ち悪い -
エラー処理をdo-catch文で対処するため、エラーレスポンス受信時はデコードしたデータモデルをthrowしている。throwするためQiitaErrorの構造体はErrorを継承している。失敗は失敗だが、例外として処理するのは正しいのだろうか?
-
例外としてではなくResultを使って成功と失敗を分けたいがやり方がわからなかった。
-
デコード失敗時を
} catch let decodeError {
で書いた場合例外を全部拾ってしまうため} catch let decodeError as DecodingError {
と書く必要があった。
こちらは気づかず解決に時間がかかった。
あとがき
Task, do ってやるとネストが深くなっていくような...
実際の業務だったら、たぶんまだAlamofireを使う。
参考