Setting up URLSession for network requests in an iOS application

NOVASOLUTIONS.TECHNOLOGY is engaged in the development, support and maintenance of iOS, Android, PWA mobile applications. We have extensive experience and expertise in publishing mobile applications in popular markets like Google Play, App Store, Amazon, AppGallery and others.
Development and support of all types of mobile applications:
Information and entertainment mobile applications
News apps, games, reference guides, online catalogs, weather apps, fitness and health apps, travel apps, educational apps, social networks and messengers, quizzes, blogs and podcasts, forums, aggregators
E-commerce mobile applications
Online stores, B2B apps, marketplaces, online exchanges, cashback services, exchanges, dropshipping platforms, loyalty programs, food and goods delivery, payment systems.
Business process management mobile applications
CRM systems, ERP systems, project management, sales team tools, financial management, production management, logistics and delivery management, HR management, data monitoring systems
Electronic services mobile applications
Classified ads platforms, online schools, online cinemas, electronic service platforms, cashback platforms, video hosting, thematic portals, online booking and scheduling platforms, online trading platforms

These are just some of the types of mobile applications we work with, and each of them may have its own specific features and functionality, tailored to the specific needs and goals of the client.

Showing 1 of 1 servicesAll 1735 services
Setting up URLSession for network requests in an iOS application
Medium
from 1 business day to 3 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_mobile-applications_feedme_467_0.webp
    Development of a mobile application for FEEDME
    756
  • image_mobile-applications_xoomer_471_0.webp
    Development of a mobile application for XOOMER
    624
  • image_mobile-applications_rhl_428_0.webp
    Development of a mobile application for RHL
    1052
  • image_mobile-applications_zippy_411_0.webp
    Development of a mobile application for ZIPPY
    947
  • image_mobile-applications_affhome_429_0.webp
    Development of a mobile application for Affhome
    862
  • image_mobile-applications_flavors_409_0.webp
    Development of a mobile application for the FLAVORS company
    445

Setting up URLSession for network requests in iOS applications

URLSession is Apple's standard network stack, and most issues here don't come from API ignorance but from incorrect configuration: the default URLSession.shared works in small examples, but in production leads to memory leaks, App Transport Security violations and hard-to-diagnose timeouts.

Where mistakes happen most often

Incorrect URLSessionConfiguration. .shared doesn't support background-transfer and doesn't allow timeout configuration at the session level. For an API client you need at minimum:

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 means URLSession creates its own serial queue. If you pass OperationQueue.main — all completion handlers run on main thread, which blocks UI during slow JSON parsing.

Ignoring URLSessionTaskDelegate when working with SSL pinning. Without a delegate, you can't override urlSession(_:didReceive:completionHandler:) to verify the certificate. I've seen countless applications where SSL-pinning is "implemented" through third-party libraries but doesn't actually work because the session was created without a delegate — a cycle preserved.

Leaks through [weak self]. URLSessionDataTask holds a strong reference to the session delegate until session.invalidateAndCancel() or finishTasksAndInvalidate() is called explicitly. If URLSession is stored as a class property and the class doesn't invalidate the session on deinit — the cycle persists.

How we build the network layer

Foundation — Protocol-Oriented approach with NetworkClient protocol, which allows mocking requests in Unit-tests without needing to run the server.

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 instead of completion handlers — this is more than syntactic sugar. Structured concurrency allows canceling requests via Task.cancel(), which automatically calls task.cancel() at the URLSession level. The old completion-block scheme didn't provide this control.

Retry logic is implemented as a wrapper, not cluttering the main client:

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 // don't retry 4xx and decoding errors
        }
    }
    throw lastError
}

Background Downloads. For file downloads — URLSessionConfiguration.background(withIdentifier:). The system can terminate the process and resume the download on next launch. Required method in AppDelegate:

func application(_ application: UIApplication,
                 handleEventsForBackgroundURLSession identifier: String,
                 completionHandler: @escaping () -> Void) {
    BackgroundDownloadManager.shared.completionHandler = completionHandler
}

Without this handler, iOS won't restart the application after background download completion.

Troubleshooting

Tools: Charles Proxy or Proxyman — for traffic inspection; Network Instrument in Xcode — for analyzing parallel connection counts and finding connection starvation; os_log with category com.apple.network — for low-level Network.framework logging.

On NSURLErrorDomain -1001 (request timed out) first check timeoutIntervalForRequest — default 60 seconds, which is unexpectedly long for a mobile app. On NSURLErrorDomain -1200 (SSL error) — check ATS policy in Info.plist and certificate chain correctness on the server via openssl s_client.

Process

Audit existing network layer: session configuration, error handling, timeouts, token authorization handling.

Design: API client with authorization support via URLSessionTaskDelegate or RequestInterceptor, handle 401 with token refresh.

Development: implementation, Unit-test coverage via mock session (URLProtocol subclass).

Testing: integration tests on real API, network behavior testing under poor conditions via Network Link Conditioner.

Timeline estimates

Task Timeline
Basic API client with async/await 1 day
+ SSL pinning + retry + token refresh 2–3 days
Migrate existing network layer 2–3 days