Библиотека для организации сетевого слоя в проекте.
- Выделяйте объекты для работы с сетью в отдельный модуль, таргет или библиотеку, чтобы они находились изолировано в своём
namespace
. - Разбивайте запросы на отдельные структуры. Классы не запрещаются, но делайте их неизменяемыми.
enum
могут подойти, если разные запросы имеют одинаковый ответ на них.
Чтобы добавить Apexy в ваш Xcode проект используя CocoaPods, укажите его в Podfile.
Если вы хотите использовать Apexy с Alamofire:
pod 'Apexy'
Если вы хотите использовать Apexy без Alamofire:
pod 'Apexy/URLSession'
Если вы хотите использовать ApexyLoader:
pod 'Apexy/Loader'
Если у вас есть Xcode проект, откройте его и выберите File → Swift Packages → Add package Dependency и вставьте адрес репозитория Apexy:
https://github.com/RedMadRobot/apexy-ios
Будут достуны 3 продукта: Apexy, ApexyAlamofire, ApexyLoader.
Apexy — Под капотом использует URLSession
ApexyAlamofire — Под капотом использует Alamofire
ApexyLoader — дополнение для Apexy, которое позволяет хранить загруженные данные в памяти и следить за состоянием загрузки. Подробности смотрите в документации ApexyLoader:
Если у вас есть Swift пакет, добавьте Apexy как зависимость в свойство dependencies файла Package.swift.
dependencies: [
.package(url: "https://github.com/RedMadRobot/apexy-ios.git")
]
Endpoint
- один из базовых протоколов организации работы с REST API. Является совокупностью запроса и обработки ответа.
Обязательно не мутабельный.
- Создает
URLRequest
для отправки запроса. - Валидирует ответ на наличие ошибок API.
- Преобразует ответ в нужный тип (
Data
,String
,Decodable
).
public struct Book: Codable, Identifiable {
public let id: String
public let name: String
}
public struct BookEndpoint: Endpoint {
public typealias Content = Book
public let id: Book.ID
public init(id: Book.ID) {
self.id = id
}
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(id)
return URLRequest(url: url)
}
public func validate(_ response: URLResponse?, with body: Data) throws {
// TODO: check API / HTTP error
}
public func content(from response: URLResponse?, with body: Data) throws -> Content {
return try JSONDecoder().decode(Content.self, from: body)
}
}
let client = Client ...
let endpoint = BookEndpoint(id: "1")
client.request(endpoint) { (result: Result<Book, Error>)
print(result)
}
Client
- объект с одним методом способный выполнить Endpoint
.
- Легко мокается, так как у него один метод.
- Легко отправить через него несколько разных
Endpoint
. - Легко оборачивается в декораторы или адаптеры. Например можно обернуть в
Combine
и вам не придется делать обертки для каждого запроса.
Разделение на Client
и Endpoint
позволяет разделить асинхронный код в Client
от синхронного кода в Endpoint
. Таким образом сайд эффекты изолированы в одном месте Client
, а чистые функции в немутабельных Endpoint
.
CombineClient
- отдельный протокол, который содержит релизацию сетевый вызовов через Combine.
ConcurrencyClient
- отдельный протокол, который содержит релизацию сетевый вызовов через Async/Await.
- По умолчанию, новые методы релизованы как надстройки над существующими методами с замыканиями.
- Для
ApexyAlamofire
методы уже реализованы через методыAlamofire
. - Для
URLSession
добавлены через реализацию системных методов через Async/Await для версий ниже iOS 15.
Client
, CombineClient
и ConcurrenyClient
- независимые протоколы. В зависимости от удобного для вас способа работы асинхронностью, вы можете выбрать конкретный протокол.
Так как большинство запросов будут получать JSON, то на уровне модуля нужно сделать базовые протоколы. Они будут содержать в себе общую логику запросов для конкретной API.
JsonEndpoint
- базовый протокол для запросов ожидающих JSON в теле ответа.
public protocol JsonEndpoint: Endpoint where Content: Decodable {}
extension JsonEndpoint {
public func validate(_ response: URLResponse?, with body: Data) throws {
// TODO: check API / HTTP error
}
public func content(from response: URLResponse?, with body: Data) throws -> Content {
return try JSONDecoder().decode(Content.self, from: body)
}
}
VoidEndpoint
базовый протокол для запросов не ожидающих тела ответа.
public protocol VoidEndpoint: Endpoint where Content == Void {}
extension VoidEndpoint {
public func validate(_ response: URLResponse?, with body: Data) throws {
// TODO: check API / HTTP error
}
public func content(from response: URLResponse?, with body: Data) throws {}
}
BookListEndpoint
- получения списка книг.
public struct BookListEndpoint: JsonEndpoint, URLRequestBuildable {
public typealias Content = [Book]
public func makeRequest() throws -> URLRequest {
return get(URL(string: "books")!)
}
}
BookEndpoint
- получения книги по ID
.
public struct BookEndpoint: JsonEndpoint, URLRequestBuildable {
public typealias Content = Book
public let id: Book.ID
public init(id: Book.ID) {
self.id = id
}
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(id)
return get(url)
}
}
UpdateBookEndpoint
- обновление книги.
public struct UpdateBookEndpoint: JsonEndpoint, URLRequestBuildable {
public typealias Content = Book
public let Book: Book
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(Book.id)
return put(url, body: .json(try JSONEncoder().encode("Book")))
}
}
Для удобства конструирования
URLRequest
вы можете использовать функции изHTTP
.
DeleteBookEndpoint
- удаление книги по ID
.
public struct DeleteBookEndpoint: VoidEndpoint, URLRequestBuildable {
public let id: Book.ID
public init(id: Book.ID) {
self.id = id
}
public func makeRequest() throws -> URLRequest {
let url = URL(string: "books")!.appendingPathComponent(id)
return delete(url)
}
}
Для отправки файлов или больших объемов данных вы можете использовать UploadEndpoint
. В методе makeRequest()
необходимо вернуть URLRequest
и загружаемые данные, это может быть файл .file(URL)
, данные .data(Data)
или поток .stream(InputStream)
. Для выполнения запроса вызовите метод Client.upload(endpoint:, completionHandler:)
. С помощью объекта Progress
вы сможете отслеживать прогресс загрузки данных либо отменить запрос.
public struct FileUploadEndpoint: UploadEndpoint {
public typealias Content = Void
private let fileUrl: URL
init(fileUrl: URL) {
self.fileUrl = fileUrl
}
public func content(from response: URLResponse?, with body: Data) throws {
// ...
}
public func makeRequest() throws -> (URLRequest, UploadEndpointBody) {
var request = URLRequest(url: URL(string: "upload")!)
request.httpMethod = "POST"
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
return (request, .file(fileUrl))
}
}
Если приложение называется Household
, то модуль с сетью будет называться HouseholdAPI
.
Разбивайте сетевой слой на папки:
Model
папка с моделями сетевого уровня. То что отправляем и то что получаем в ответах.Endpoint
папка с запросами.Common
общие хелперы. НапримерAPIError
.
- Household
- HouseholdAPI
- Model
- Book
- Endpoint
JsonEndpoint
VoidEndpoint
- Book
BookListEndpoint
BookEndpoint
UpdateBookEndpoint
DeleteBookEndpoint
- Common
APIError
- Model
- HouseholdAPITests
- Endpoint
Book
BookListEndpointTests
BookEndpointTests
UpdateBookEndpointTests
DeleteBookEndpointTests
- Endpoint
- iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
- Xcode 12+
- Swift 5.3+