Skip to Content
Course content

82: URLSessionDelegate for Advanced Control

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I remember a developer I mentored a few years back who was building a high-res photo backup tool. He had everything working perfectly with the standard dataTask(with:completionHandler:) approach, but there was one glaring problem: for a 20MB upload, the UI just... sat there. The user had no idea if the app had crashed or if the upload was actually happening. He kept trying to find a "progress" parameter in the completion handler, not realizing that by the time that closure fires, the work is already done. He was trying to track a journey by only looking at the destination.

When you need to know what's happening while a request is in flight—whether that's updating a progress bar, handling a specific authentication challenge, or managing a background download—the completion handler is too blunt an instrument. This is where URLSessionDelegate comes in. Instead of a single callback at the end, the delegate pattern turns the network request into a conversation. The session calls you back at specific milestones throughout the request's lifecycle.

Hooking into the Request Lifecycle

To move beyond the basic completion handler, you have to initialize your URLSession with a delegate. Most of the time, you'll create a custom class that inherits from NSObject and adopts one of the specialized delegate protocols, like URLSessionDataDelegate or URLSessionTaskDelegate. I usually recommend adopting URLSessionTaskDelegate first because it handles the "big picture" events, like when a task finishes or fails.

class NetworkManager: NSObject, URLSessionTaskDelegate, URLSessionDataDelegate {
    lazy var session: URLSession = {
        // We pass 'self' as the delegate here
        return URLSession(configuration: .default, delegate: self, delegateQueue: .main)
    }()

    func startUpload(url: URL, fileData: Data) {
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        let task = session.uploadTask(with: request, from: fileData)
        task.resume()
    }
}

Notice that I used delegateQueue: .main. Since you'll likely be updating the UI with the progress you're about to track, specifying the main queue here saves you from wrapping every single delegate call in a DispatchQueue.main.async block. It's a small quality-of-life win that keeps the code much cleaner.

Tracking Byte-by-Byte Progress

If you want to build that progress bar my mentee was struggling with, you need URLSessionTaskDelegate. Specifically, the urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:) method. This method is called repeatedly as the system pushes chunks of data to the server. It's the heartbeat of your upload.

func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
    let progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
    print("Upload Progress: \(String(format: "%.2f", progress * 100))%")
    // Here is where you'd update your UI progress bar
}

One thing to watch out for: totalBytesExpectedToSend can sometimes be -1 if the server doesn't know the size of the payload upfront. Always check for that, or your app will crash trying to divide by zero. It's one of those "gotchas" that doesn't show up in the documentation but will definitely show up in your crash logs.

Intercepting Authentication Challenges

Beyond progress bars, the delegate is your only real way to handle "Advanced Control" scenarios like SSL pinning or custom server trust. When a server presents a certificate that the OS doesn't automatically trust, URLSession doesn't just fail; it asks the delegate: "Hey, do you trust this?"

You do this by implementing urlSession(_:didReceive:completionHandler:). This is where you can inspect the challenge object. If you're implementing a high-security app, you can compare the server's public key against a known copy bundled in your app. If they match, you call the completion handler with .useCredential; if they don't, you cancel the request immediately to prevent a man-in-the-middle attack.

It feels a bit verbose compared to a simple closure, but that's the trade-off. You're exchanging brevity for total visibility into the network stack.




📋 Practical Task

Build a Real-Time File Upload Monitor

Create a class called UploadMonitor that manages a file upload and reports progress to the console. Your implementation must meet the following requirements:

  • Adopt NSObject, URLSessionTaskDelegate, and URLSessionDataDelegate.
  • Initialize a URLSession with self as the delegate and use the main queue.
  • Implement urlSession(_:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:) to calculate the percentage of the upload completed.
  • The progress should be printed to the console as a percentage (e.g., "Upload: 45.2%").
  • Implement a method uploadLargeFile(url: URL, data: Data) that starts a uploadTask.
  • Add a safeguard to handle cases where totalBytesExpectedToSend is less than or equal to zero to avoid division errors.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.