Налаштування URLSession для мережевих запитів у iOS-додатках
URLSession — стандартний мережевий стек Apple, і більшість проблем виникає не від незнання API, а від неправильної конфігурації: стандартний URLSession.shared працює в малих прикладах, але в продакшені призводить до утечок пам'яті, порушень App Transport Security та важко діагностувальних таймаутів.
Де найчастіше допускаються помилки
Неправильна URLSessionConfiguration. .shared не підтримує фонове завантаження та не дозволяє налаштовувати таймаути на рівні сесії. Для 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 створює власну послідовну чергу. Якщо ви передадите 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 — це більше, ніж синтаксичний цукор. Структурована конкурентність дозволяє скасовувати запити через Task.cancel(), який автоматично викликає task.cancel() на рівні URLSession. Старий спосіб з completion-блоками не давав такого контролю.
Логіка повторних спроб реалізується як обгортка, а не засмічення основного клієнта:
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
}
Фонові завантаження. Для завантажень файлів — 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 — для аналізу кількості паралельних з'єднань та пошуку завантаження з'єднань; 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 дні |







