URLProtocol

Shilpa Bansal
4 min readNov 30, 2021

URLProtocol lets you redefine how Apple’s URL Loading System operates, by defining custom URL schemes and redefining the behavior of existing URL schemes.

I will be covering 2 use cases where URLProtocol can make life much easier.

1. Adding logs for all network requests:

For different projects, different network libraries are used. With various modules in place, it becomes difficult to debug all network calls that are going through the app. Charles is one way to see the logs, but the kind of constraints it has with VPN and others, sometimes it becomes too difficult to enable the logs.

URLProtocol is an easy and simple solution and it works irrespective of the n/w library.

The small piece of code as mentioned below can log all network calls:

final class NetworkLogsProtocol: URLProtocol {

override public class func canInit(with request: URLRequest) -> Bool {

print(“URLProtocol method: \(request.httpMethod ?? “”) \nurl: \(request.url?.absoluteString ?? “”)”)

print(“URLProtocol body: \(request.httpBody ?? “”)”)

print(“URLProtocol headerFields: \(request.allHTTPHeaderFields ?? “”)”)

// By returning `false`, this URLProtocol will do nothing less than logging.

return false

}

}

URLProtocol.registerClass(NetworkLogsProtocol.self)

The above code snippet is performing the below tasks:

  1. NetworkLogsProtocol class extends URLProtocol. As URLProtocol is an abstract class, some class has to extend it to use URLProtocol.
  2. Every time the URL Loading System receives a request to load a URL, it searches for a registered protocol handler to handle the request. Each handler tells the system whether it can handle a given request via its canInit(_:) method or not. On returning false, the default network calls happen.
  3. Registering extending class: It is a mandatory step to see the logs. It needs to be added in-app delegate or at someplace which gets executed before making any calls.

2. Redirecting the network calls

To understand how it works, let's take a small playground app that makes the async network call and print the response.

1. import UIKit

2. import PlaygroundSupport

3. PlaygroundPage.current.needsIndefiniteExecution = true

4. let requestUrl = URL(string: “https://ile-api.essentialdeveloper.com/essential-feed/v1/feed")!

5. var request = URLRequest(url: requestUrl)

6. request.httpMethod = “GET”

7. URLSession.shared.dataTask(with: request) { (data, response, error) in

8. if let data = data,

9. let dataString = try? JSONDecoder().decode(FeedItems.self, from: data) {

10. print(“Original response: \(dataString)”)

11. }

12. PlaygroundPage.current.finishExecution()

13. }.resume()

Some of you might be new to lines 2, 3, 12. By default in the playground, all top-level code is executed, and then execution is terminated. When working with asynchronous code, enable indefinite execution to allow execution to continue after the end of the playground’s top-level code is reached. This, in turn, gives threads and callbacks time to execute.

// import the module

import PlaygroundSupport

// write this at the beginning

PlaygroundPage.current.needsIndefiniteExecution = true

// To finish execution

PlaygroundPage.current.finishExecution()

Below code can be used to parse the data received from n/w calls:

struct FeedItems: Decodable, CustomStringConvertible {

let items: [Feeds]

private enum CodingKeys: String, CodingKey {

case items

}

var description: String {

var result = “”

items.enumerated().forEach { (index, item) in

result += “\(index+1) \(item.id) : \(item.subtitle ?? “”) \n”

}

return result

}

}

struct Feeds: Decodable {

let id: String

let subtitle: String?

let location: String?

let image: String?

private enum CodingKeys: String, CodingKey {

case id

case subtitle = “description”

case location

case image

}

}

The below code might look similar to the first example:

class TestURLProtocol: URLProtocol {

override class func canInit(with request: URLRequest) -> Bool {

return true

}

override class func canonicalRequest(for request: URLRequest) -> URLRequest {

return request

}

override func startLoading() {

if let jsonData = jsonData {

self.client?.urlProtocol(self, didLoad: jsonData)

}

client?.urlProtocolDidFinishLoading(self)

}

override func stopLoading() {

}

private var jsonData: Data? {

let JSON = “””

{

“items”: [

{

“id”: “TestID”,

“description”: “Test description”,

“location”: “test location”,

“image”: “test image”

}

]

}

“””

return JSON.data(using: .utf8)!

}

}

In the above example, the following steps are being performed:

  1. One class TestURLProtocol extends URLProtocol
  2. Registers TestURLProtocol
  3. canInit: (As mentioned above) Every time the URL Loading System receives a request to load a URL, it searches for a registered protocol handler to handle the request. Each handler tells the system whether it can handle a given request via its canInit(_:) method or not. On returning false, the default network calls happen.
  4. canonicalRequest is a representation of the real request. Usually, there is no need of changing the request, in this case, the real request can be returned.
  5. The loading system uses startLoading() and stopLoading() to tell URLProtocol to start and stop handling a request. The caches/mocked response can be passed in startLoading method.

When To Use URLProtocol?

  1. Provide Custom Responses for Network Requests and for unit testing:

It doesn’t matter if you’re making a request using a UIWebView, URLConnection, or even using a third-party library (like AFNetworking, MKNetworkKit, your own, etc, as these are all built on top of URLConnection). You can provide a custom response, both for metadata and for data.

2. Skip Network Activity and Provide Local Data:

Sometimes it’s unnecessary to fire a network request to provide the app whatever data it needs. URLProtocol can set your app up to find data on local storage or in a local database.

3. Redirect the network requests to a proxy server:

4. Change the meta-data of requests:

Before firing any network request, you can decide to change its metadata or data.

5. Use Your Own Networking Protocol:

You may have your own networking protocol (for instance, something built on top of UDP). You can implement it and, in your application, you still can keep using any networking library you prefer.

Reference:

https://www.raywenderlich.com/2292-using-nsurlprotocol-with-swift

--

--

Shilpa Bansal

Staff Software Engineer, Health and Wellness, Sam’s Club