Универсальный веб-сервис
Практически всегда запросы к веб-сервисам имеют однотипную логику, поддающуюся унификации через дженерики. Одним из вариантов такого подхода я бы и хотел поделиться.
Основная логика ляжет в родительский класс Webservice:
import Foundation
class Webservice {
enum Method {
case get([URLQueryItem]?)
case post(Data?)
var name: String {
switch self {
case .get:
return "GET"
case .post:
return "POST"
}
}
}
struct Header {
let name: String
let value: String
static let jsonContentType = Header(
name: "Content-Type",
value: "application/json; charset=UTF-8"
)
}
let baseURL: URL
let urlSession: URLSession
let jsonDecoder: JSONDecoder
init(
baseURL: URL,
urlSession: URLSession = .shared,
jsonDecoder: JSONDecoder = JSONDecoder()
) {
self.baseURL = baseURL
self.urlSession = urlSession
self.jsonDecoder = jsonDecoder
}
func load<Entity: Codable, Path: CustomStringConvertible>(
_ type: Entity.Type,
path: Path,
method: Method = .get(nil),
headers: [Header] = [.jsonContentType]
) async throws -> Entity {
var urlRequest: URLRequest
guard let url = URL(string: path.description, relativeTo: baseURL) else {
throw URLError(.badURL)
}
switch method {
case .get(let queryItems):
var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)
if urlComponents?.queryItems == nil {
urlComponents?.queryItems = queryItems
} else {
urlComponents?.queryItems?.append(contentsOf: queryItems ?? [])
}
guard let url = urlComponents?.url else {
throw URLError(.badURL)
}
urlRequest = URLRequest(url: url)
case .post(let data):
urlRequest = URLRequest(url: url)
urlRequest.httpBody = data
}
urlRequest.httpMethod = method.name
for header in headers {
urlRequest.addValue(header.value, forHTTPHeaderField: header.name)
}
let (data, response) = try await urlSession.data(for: urlRequest)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw URLError(.badServerResponse)
}
return try jsonDecoder.decode(Entity.self, from: data)
}
}
А конкретные методы определяются уже в веб-сервисах, наследующихся от него. В данном случае PhotosWebservice и метод загрузки фотографий:
import Foundation
class PhotoWebservice: Webservice {
enum Path: CustomStringConvertible {
case photos
case user(id: Int)
var description: String {
switch self {
case .photos: return "photos"
case .user(let id): return "user/\(id)"
}
}
}
func loadPhotos() async throws -> [Photo] {
return try await load([Photo].self, path: Path.photos)
}
}