Полная документация API библиотеки EKNetwork.
- NetworkManager
- NetworkRequest
- HTTPMethod
- RequestBody
- MultipartFormData
- RetryPolicy
- NetworkProgress
- TokenRefreshProvider
- Стриминг (NDJSON / SSE)
- Типы ошибок
- Типы ответов
- UserAgentConfiguration
Основной класс для управления сетевыми запросами.
public init(
baseURL: @escaping (() -> URL),
session: URLSessionProtocol = URLSession.shared,
streamingSession: URLSessionStreamingProtocol? = nil,
loggerSubsystem: String = "com.yourapp.networking",
userAgentConfiguration: UserAgentConfiguration? = nil,
responseDecoderProvider: (() -> JSONDecoder)? = nil
)Параметры:
baseURL: Замыкание, возвращающее базовый URL для каждого запроса. Используйте{ myURL }для фиксированного URL или замыкание, читающее из конфига/окружения, для динамического базового URL (без гонок при переключении окружений).session:URLSessionProtocolдля выполнения запросов (по умолчаниюURLSession.shared)streamingSession: Опциональная сессия дляstream(_:accessToken:)(NDJSON / SSE / chunked transfer). Если не передана, менеджер используетsession, если та поддерживаетURLSessionStreamingProtocol(URLSessionподдерживает по умолчанию), иначе —URLSession.shared. Добавлено в 1.6.0; существующие вызовы инициализатора остаются совместимыми.loggerSubsystem: Идентификатор подсистемы для экземпляраLoggeruserAgentConfiguration: Опциональная конфигурация User-AgentresponseDecoderProvider: Опциональный глобальный JSON-декодер для ответов (может переопределять декодирование запросов)
Пример:
// Фиксированный базовый URL
let manager = NetworkManager(baseURL: { URL(string: "https://api.example.com")! })
// Динамический базовый URL (например, из настроек)
let manager = NetworkManager(baseURL: { AppSettings.shared.apiBaseURL })Замыкание, возвращающее базовый URL; вызовите baseURL() для получения текущего базового URL. Каждый запрос вызывает это замыкание, поэтому URL может меняться между запросами без гонок.
Опциональный обновлятель токенов для обработки обновления токенов аутентификации. При установке автоматически обновляет токены при ответах 401.
Конфигурация User-Agent. При установке автоматически добавляет заголовок User-Agent ко всем запросам.
Опциональный глобальный декодер JSON-ответов. Если задан, может переопределять декодирование на уровне запросов.
Отправляет сетевой запрос и декодирует ответ.
Параметры:
request: Сетевой запрос для отправкиaccessToken: Опциональное замыкание, возвращающее токен доступа для аутентификации
Возвращает: Декодированный ответ типа T.Response
Выбрасывает: Ошибки, возникшие во время запроса или декодирования
Пример:
let response = try await manager.send(
SignInRequest(email: "user@example.com", password: "password"),
accessToken: { TokenStore.shared.accessToken }
)Протокол, представляющий сетевой запрос. Соответствующие типы определяют путь запроса, метод, заголовки, параметры и тип ответа.
Ожидаемый тип ответа, должен соответствовать Decodable.
Компонент пути, добавляемый к базовому URL.
HTTP метод для запроса.
Опциональные HTTP заголовки для включения в запрос. По умолчанию nil.
Опциональные параметры запроса, добавляемые к URL. По умолчанию nil.
Заголовок Content-Type для запроса. По умолчанию "application/json".
Опциональное тело, отправляемое с запросом, поддерживающее различные кодировки. По умолчанию nil.
Опциональные данные multipart формы для запросов на загрузку. По умолчанию nil.
Опциональный наблюдатель прогресса для загрузки/выгрузки. По умолчанию nil.
Политика повторных попыток для применения к этому запросу. По умолчанию RetryPolicy().
Опциональный декодер ошибок для извлечения ответов об ошибках с сервера. По умолчанию nil.
Должен ли запрос разрешать повторные попытки и обновление токена при 401 Unauthorized? По умолчанию true.
Опциональный обработчик, используемый, когда сервер возвращает пустое тело. По умолчанию nil.
Когда сервер отвечает успешным кодом и нулевой длиной тела, NetworkRequest вызывает этот обработчик вместо JSON-декодирования. Если оставить nil, decodeResponse выбросит NetworkError.emptyResponse.
Выбор подходящего подхода для пустых ответов:
-
Используйте
EmptyResponse(рекомендуется для простых случаев успеха):struct DeleteRequest: NetworkRequest { typealias Response = EmptyResponse // emptyResponseHandler предоставляется автоматически }
Лучше всего для конечных точек, которые возвращают 204 No Content или пустые тела, когда вам нужно только подтвердить успех. Реализация по умолчанию игнорирует любые данные и возвращает
EmptyResponse(). -
Используйте
StatusCodeResponse(когда нужны HTTP метаданные):struct UpdateRequest: NetworkRequest { typealias Response = StatusCodeResponse // emptyResponseHandler автоматически извлекает код состояния и заголовки }
Лучше всего, когда вам нужно проверить HTTP код состояния или заголовки из ответа. Реализация по умолчанию копирует код состояния и заголовки из
HTTPURLResponse. -
Предоставьте кастомный
emptyResponseHandler(для продвинутых случаев):struct CustomRequest: NetworkRequest { typealias Response = MyCustomResponse var emptyResponseHandler: ((HTTPURLResponse) throws -> MyCustomResponse)? { { response in MyCustomResponse( status: response.statusCode, customHeader: response.value(forHTTPHeaderField: "X-Custom") ) } } }
Нужно только когда вы должны сформировать кастомный тип ответа из заголовков, кода состояния или других метаданных, сопровождающих пустой payload.
Предоставляет экземпляр декодера для JSON ответов. По умолчанию JSONDecoder().
Предоставляет экземпляр кодировщика для JSON тел запросов. По умолчанию JSONEncoder().
Разрешает ли NetworkManager переопределять декодирование этого запроса при наличии глобального декодера. По умолчанию true.
Декодирует сырой ответ в связанный тип ответа.
Параметры:
data: Данные ответаresponse: URL ответ
Возвращает: Декодированный ответ типа Response
Выбрасывает: Ошибки декодирования
Реализация по умолчанию: Обрабатывает JSON декодирование и резервные варианты для пустых ответов.
Перечисление, представляющее HTTP методы, поддерживаемые сетевой прослойкой.
public enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case put = "PUT"
case delete = "DELETE"
case patch = "PATCH"
case head = "HEAD"
case options = "OPTIONS"
case trace = "TRACE"
case connect = "CONNECT"
}Представляет тело запроса для сетевого запроса, поддерживающее различные типы.
Создает тело запроса из кодируемого объекта (обычно для JSON).
Параметры:
encodable: Кодируемый объект для кодированияcontentType: Тип контента (по умолчанию"application/json")
Создает тело запроса из сырых данных.
Параметры:
data: Сырые данныеcontentType: Тип контента
Создает тело запроса из потока ввода (для больших загрузок).
Параметры:
stream: Поток вводаcontentType: Тип контента
Создает тело запроса с кодированием формы URL.
Параметры:
parameters: Пары ключ-значение для данных формы URL
Тип контента: Автоматически устанавливается в "application/x-www-form-urlencoded"
RequestBody поддерживает следующие типы контента:
.encodable(Encodable)- JSON-кодируемый объект.raw(Data)- Сырые двоичные или предварительно закодированные данные.stream(InputStream)- Поток для больших загрузок данных.formURLEncoded([String: String])- Пары ключ-значение для данных формы URL
Представляет данные multipart формы для загрузки файлов.
Уникальная строка границы, используемая для разделения частей. Автоматически генерируется как UUID.
Массив частей, включенных в multipart форму.
Добавляет новую часть к данным multipart формы.
Параметры:
name: Имя поля формыdata: Содержимое данныхmimeType: Строка MIME типаfilename: Опциональное имя файла
Кодирует данные multipart формы в объект Data, подходящий для HTTP тела.
Возвращает: Закодированные данные, представляющие multipart форму
public struct Part {
public let name: String
public let filename: String?
public let data: Data
public let mimeType: String
}Определяет поведение повторных попыток для сетевых запросов.
Максимальное количество попыток повторной попытки.
Задержка в секундах перед повторной попыткой запроса.
Замыкание для определения, должна ли быть повторная попытка на основе возникшей ошибки.
public init(
maxRetryCount: Int = 0,
delay: TimeInterval = 1.0,
shouldRetry: @escaping (Error) -> Bool = { /* реализация по умолчанию */ }
)Поведение по умолчанию:
- Не повторяет при
NetworkError.unauthorized - Не повторяет при
URLError.userAuthenticationRequired - Не повторяет при пользовательских ошибках API (типы, содержащие "APIError", "ServerError" или "Business")
- Повторяет при других ошибках
Наблюдаемый объект для отслеживания прогресса загрузки или выгрузки сети.
Доля выполненной задачи, от 0.0 до 1.0.
@MainActor
class UploadViewModel: ObservableObject {
@Published var uploadProgress: Double = 0.0
func uploadFile(_ data: Data) async throws {
let progress = NetworkProgress()
progress.$fractionCompleted
.assign(to: &$uploadProgress)
// Используйте progress в запросе
struct UploadRequest: NetworkRequest {
var progress: NetworkProgress? { progress }
// ...
}
}
}Протокол для предоставления функциональности обновления токенов.
Обновляет токен аутентификации при необходимости. Этот метод вызывается автоматически при получении ответа 401 Unauthorized.
Пример:
class TokenManager: TokenRefreshProvider {
func refreshTokenIfNeeded() async throws {
let refreshRequest = RefreshTokenRequest(
refreshToken: TokenStore.shared.refreshToken
)
let response = try await networkManager.send(refreshRequest, accessToken: nil)
TokenStore.shared.accessToken = response.accessToken
}
}Доступно начиная с 1.6.0.
send(_:accessToken:) рассчитан на эндпоинты, отдающие тело целиком одним Decodable-объектом. Для эндпоинтов, которые отдают данные постепенно — newline-delimited JSON, Server-Sent Events, chunked-логи / inference-стримы — используйте stream(_:accessToken:). Стриминг переиспользует тот же самый пайплайн построения запроса (заголовки, Authorization, User-Agent, тело, baseURL), что и send(_:). То есть прикладному коду никогда не нужно собирать URLRequest вручную и рисковать потерять обязательные заголовки вроде X-Device-ID или кастомной авторизации.
public protocol NetworkStreaming: AnyObject {
func stream<T: NetworkRequest>(
_ request: T,
accessToken: (() -> String?)?
) async throws -> StreamingResponse
}NetworkManager соответствует и NetworkManaging, и NetworkStreaming. Существующие моки NetworkManaging остаются рабочими.
public struct StreamingResponse: Sendable {
public let statusCode: Int
public let headers: [String: String]
public let bytes: AsyncThrowingStream<UInt8, Error>
public func lines() -> AsyncThrowingStream<String, Error>
public func ndjson<Item: Decodable & Sendable>(
as itemType: Item.Type,
decoder: JSONDecoder = JSONDecoder()
) -> AsyncThrowingStream<Item, Error>
}bytes— поток сырых байт (по одномуUInt8), в порядке прихода.lines()— UTF-8 строки, разделённые\n, с обрезкой\r(CRLF-aware), пустые строки пропускаются. Корректно собирает многобайтовые UTF-8 последовательности, разрезанные TCP-сегментами.ndjson(as:decoder:)— по одномуDecodable-объекту на каждую непустую строку. Битая строка — стрим завершается ошибкой.
Отмена пробрасывается автоматически: выход из for try await или отмена внешнего Task отменяет сетевую задачу.
public protocol URLSessionStreamingProtocol: Sendable {
func byteStream(for request: URLRequest) async throws -> (AsyncThrowingStream<UInt8, Error>, URLResponse)
}URLSession соответствует протоколу по умолчанию (мостит URLSession.bytes(for:) в полностью Sendable-стрим). Реализуйте этот протокол в моках, если хотите тестировать стриминговый пайплайн без сети.
| Аспект | send(_:) |
stream(_:) |
|---|---|---|
| Заголовки, тело, авторизация | buildURLRequest |
buildURLRequest (та же точка) |
| 401 → refresh + retry | один раз, если allowsRetry == true |
один раз, до того как пришёл хоть один байт тела |
| 401 в середине стрима | n/a | не ретраится (тело уже начали отдавать) |
| Не-2xx ошибка | HTTPError / errorDecoder |
drain ≤1 МиБ, затем HTTPError / errorDecoder |
| RetryPolicy | применяется | не применяется (стрим нельзя детерминированно проиграть заново) |
| NetworkProgress | применяется | не применяется |
public enum StreamingError: Error, Equatable {
case invalidResponse // ответ не HTTPURLResponse
case errorPayloadTooLarge(limitBytes: Int) // тело не-2xx ответа превысило 1 МиБ
}struct PlayerSearchRequest: NetworkRequest {
typealias Response = EmptyResponse // не используется в стриминге
var path: String { "/api/v1/players/search" }
var method: HTTPMethod { .get }
var queryParameters: [String: String]? { ["q": query, "stream": "true"] }
var headers: [String: String]? { DeviceHeaders.current() }
let query: String
}
let response = try await manager.stream(
PlayerSearchRequest(query: "Бобр"),
accessToken: { TokenStore.shared.accessToken }
)
for try await item in response.ndjson(as: SearchEvent.self) {
handle(item) // отрисовка по мере поступления
if case .end = item { break }
}let response = try await manager.stream(MyEventsRequest(), accessToken: nil)
for try await line in response.lines() {
guard line.hasPrefix("data:") else { continue }
let payload = line.dropFirst("data:".count).trimmingCharacters(in: .whitespaces)
process(payload)
}Ошибки, которые могут возникнуть во время сетевых операций.
public enum NetworkError: Error {
case invalidURL // URL не может быть построен
case emptyResponse // Данные ответа были пустыми
case unauthorized // Неавторизованный доступ, обычно HTTP 401
case invalidResponse // Ответ отсутствовал или имел неожиданный тип
case conflictingBodyTypes // И body, и multipartData установлены
}Общая HTTP ошибка, несущая код состояния и полезную нагрузку для диагностики.
public struct HTTPError: LocalizedError {
public let statusCode: Int
public let data: Data
public let headers: [String: String]
public var errorDescription: String? {
"Запрос завершился с кодом состояния \(statusCode)"
}
}Удобный ответ, который только раскрывает HTTP код состояния и заголовки.
public struct StatusCodeResponse: Decodable, Equatable {
public let statusCode: Int
public let headers: [String: String]
}Используйте этот тип, когда вам важны только код состояния и заголовки, а тело можно игнорировать. Стандартный emptyResponseHandler для Response == StatusCodeResponse копирует код и заголовки из пустого HTTPURLResponse, поэтому вы получаете эти значения без декодирования тела.
Представляет пустую полезную нагрузку. Полезно для конечных точек, которые только сигнализируют об успехе через код состояния.
public struct EmptyResponse: Decodable, Equatable {
public init() {}
}EmptyResponse годится для сценариев, где сервер возвращает 204/пустое тело и вам нужно только подтвердить успех. Реализация decodeResponse по умолчанию сразу возвращает EmptyResponse() и игнорирует payload, так что вы можете рассматривать этот тип как маркер void-успеха.
Конфигурация для генерации заголовка User-Agent.
appName: String- Имя приложенияappVersion: String- Версия приложенияbundleIdentifier: String- Идентификатор пакетаbuildNumber: String- Номер сборкиosVersion: String- Версия iOS/OSnetworkVersion: String- Версия фреймворка EKNetwork
public init(
appName: String? = nil,
appVersion: String? = nil,
bundleIdentifier: String? = nil,
buildNumber: String? = nil,
osVersion: String? = nil,
networkVersion: String? = nil
)Все параметры опциональны и по умолчанию берутся из Bundle.main или системных значений по умолчанию.
Генерирует строку User-Agent в формате:
AppName/Version (BundleID; build:BuildNumber; Platform OSVersion) EKNetwork/Version
Абстракция протокола для NetworkManager, позволяющая мокирование и внедрение зависимостей.
public protocol NetworkManaging {
var tokenRefresher: TokenRefreshProvider? { get set }
func send<T: NetworkRequest>(_ request: T, accessToken: (() -> String?)?) async throws -> T.Response
}Абстракция протокола для URLSession, позволяющая мокирование и внедрение зависимостей.
public protocol URLSessionProtocol {
func data(for request: URLRequest) async throws -> (Data, URLResponse)
}URLSession по умолчанию соответствует этому протоколу.