Настройка URLSession для сетевых запросов в iOS-приложении
URLSession — стандартный сетевой стек Apple, и большинство проблем здесь возникает не от незнания API, а от неправильной конфигурации: дефолтный URLSession.shared работает в мелких примерах, но в продакшене приводит к утечкам памяти, нарушению политики App Transport Security и сложно диагностируемым таймаутам.
Где чаще всего ошибаются
Неправильный URLSessionConfiguration. .shared не поддерживает background-transfer и не даёт настроить таймауты на уровне сессии. Для API-клиента нужно минимум:
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
config.timeoutIntervalForResource = 60
config.requestCachePolicy = .reloadIgnoringLocalCacheData
config.urlCache = nil
let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
delegateQueue: nil означает, что URLSession создаст свою serial-очередь. Если передать OperationQueue.main — все completion handlers будут выполняться на main thread, что заблокирует UI при медленном разборе JSON.
Игнорирование URLSessionTaskDelegate при работе с SSL pinning. Без делегата невозможно переопределить urlSession(_:didReceive:completionHandler:) для проверки сертификата. Сколько приложений я видел, где SSL-pinning «реализован» через стороннюю библиотеку, но фактически не работает, потому что сессия создана без делегата — не счесть.
Утечки через [weak self]. URLSessionDataTask удерживает strong-ссылку на делегат сессии до явного вызова session.invalidateAndCancel() или finishTasksAndInvalidate(). Если URLSession хранится как свойство класса, а сам класс при deinit не инвалидирует сессию — цикл сохраняется.
Как мы строим сетевой слой
Основа — Protocol-Oriented подход с NetworkClient протоколом, что позволяет мокировать запросы в Unit-тестах без необходимости поднимать сервер.
protocol NetworkClient {
func send<T: Decodable>(_ request: URLRequest) async throws -> T
}
final class URLSessionNetworkClient: NetworkClient {
private let session: URLSession
private let decoder: JSONDecoder
init(session: URLSession = .init(configuration: .default)) {
self.session = session
self.decoder = JSONDecoder()
self.decoder.keyDecodingStrategy = .convertFromSnakeCase
self.decoder.dateDecodingStrategy = .iso8601
}
func send<T: Decodable>(_ request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
guard let http = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200..<300).contains(http.statusCode) else {
throw NetworkError.httpError(statusCode: http.statusCode, data: data)
}
return try decoder.decode(T.self, from: data)
}
}
Async/await вместо completion handlers — это не просто синтаксический сахар. Structured concurrency позволяет отменять запросы через Task.cancel(), который автоматически вызывает task.cancel() на уровне URLSession. Старая схема с completion-блоками не давала такого контроля.
Retry-логика реализуется через обёртку, а не засорение основного клиента:
func sendWithRetry<T: Decodable>(
_ request: URLRequest,
maxAttempts: Int = 3,
delay: Duration = .seconds(1)
) async throws -> T {
var lastError: Error
for attempt in 0..<maxAttempts {
do {
return try await send(request)
} catch NetworkError.httpError(let code, _) where code >= 500 {
lastError = NetworkError.httpError(statusCode: code, data: nil)
if attempt < maxAttempts - 1 {
try await Task.sleep(for: delay * Double(attempt + 1))
}
} catch {
throw error // не ретраим 4xx и ошибки декодирования
}
}
throw lastError
}
Background Downloads. Для загрузки файлов — URLSessionConfiguration.background(withIdentifier:). Система может завершить процесс и возобновить загрузку при следующем запуске. Обязательный метод в AppDelegate:
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
BackgroundDownloadManager.shared.completionHandler = completionHandler
}
Без этого обработчика iOS не перезапустит приложение после завершения фоновой загрузки.
Диагностика проблем
Инструменты: Charles Proxy или Proxyman — для инспекции трафика; Network Instrument в Xcode — для анализа количества параллельных соединений и поиска connection starvation; os_log с категорией com.apple.network — для низкоуровневого логирования Network.framework.
При ошибке NSURLErrorDomain -1001 (request timed out) первым делом смотреть на timeoutIntervalForRequest — по умолчанию 60 секунд, что неочевидно много для мобильного приложения. При NSURLErrorDomain -1200 (SSL error) — проверить ATS-политику в Info.plist и корректность цепочки сертификатов на сервере через openssl s_client.
Процесс работы
Аудит существующего сетевого слоя: конфигурация сессии, обработка ошибок, таймауты, работа с токенами авторизации.
Проектирование: API-клиент с поддержкой авторизации через URLSessionTaskDelegate или RequestInterceptor, обработка 401 с обновлением токена.
Разработка: реализация, покрытие Unit-тестами через mock-сессию (URLProtocol subclass).
Тестирование: интеграционные тесты на реальном API, проверка поведения при нестабильной сети через Network Link Conditioner.
Ориентиры по срокам
| Задача | Срок |
|---|---|
| Базовый API-клиент с async/await | 1 день |
| + SSL pinning + retry + token refresh | 2–3 дня |
| Миграция существующего сетевого слоя | 2–3 дня |







