image

Daily, Vkontakte users exchanged 10 billion messages. They send each other pictures, comics, memes and other attachments. Describe how an iOS app, we decided to upload pictures using the URLProtocol, and step by step look at how to implement your.
About half a year ago in the middle was the development of a new section messages in the application VK app for iOS. This is the first section written entirely in Swift. It is located in a separate module vkm (VK Messages) who knows nothing about the unit the main application. It can even be run in a separate project — the core functionality of reading and sending messages at the same time continue to work. In the main application controllers messages are added using the appropriate Container View Controller to display, for example, the list of conversations or messages in the conversation.

The message is one of the most popular sections of the mobile application Vkontakte, so it is important that it worked like clockwork. The draft messages we are fighting for every line of code. We have always loved how neatly the messages built into the app, and we strive to ensure that it stays that way.

Gradually filling the section with new features, we came to the next task: it was necessary to make a photograph which is attached to the message, first appearing in the draft, and after sending in the list of messages. We could just add the module to work with PHImageManager, but the additional conditions made it more difficult.

image

When selecting the image user can his process: to apply a filter, rotate, crop, etc. In Annex VK such functionality is implemented in a separate component AssetService. Now we had to learn to work with it from the draft messages.

Well, the problem is fairly simple to do. This here is about the decision average, because variations weight. Take the Protocol, put it in messages and begin to fill methods. Added to AssetService, adapt the Protocol and add your implementation of the CACHE! for the viscosity. Then a recorded realization in messages added to any service or Manager that will work with all of this, and start to use. Also, there is a new developer and while trying to understand all this, sentences in a whisper… (well you get the idea). With this on his forehead already sweat appears.

image

The decision for us was not to your taste. Appear new entity, you need to know about the components of the messages when working with images from AssetService. The developer will also need to undertake further work to understand how to construct the system. Finally, there is an additional implicit tie main project components, which we try to avoid that section of messages and then continued to work as an independent module.

I wanted to solve the task so that the project knew nothing about what a picture is selected, how it is stored, does it need special way to load and render. In this case we already have the ability to boot a regular image from the Internet, but they are loaded not through an additional service, but just the URL. And, in fact, the difference between these two types of images there. Just the ones stored locally, and others on the server.

So we came to a very simple idea: what if local assets, too, can learn how to upload via URL? It seems that one click of Thanos fingers would solve all our problems: it is not necessary to know anything about AssetService, to add new data types and wasted to increase entropy, to learn to upload new images, and to take care of the data caching. Sounds like a plan.

All we need is URL

We have thought about this idea and decided to determine the format of the URLthat can be used to load local assets:

asset://?id=123&width=1920&height=1280

As id we use the value of the property localIdentifier from PHObject, and for loading images with the desired size will pass the parameters width and height. Also add some more options like crop, filter, rotatethat will allow you to work with information processed image.

To handle such URLS , we will create AssetURLProtocol:

class AssetURLProtocol: URLProtocol {
}

Its task is to load the image through the AssetService , and returning ready-to-use data.

All this will allow us to almost completely delegate work URLProtocol and the URL Loading System.

Inside messages can operate on the most common URL, just a different format. It will also be possible to reuse the existing mechanism for loading the images, it’s easy to serialize into the database and data caching to implement using standard URLCache.

Got it? If, by reading this article, you can in the application Vkontakte to attach a photo from your gallery, then Yes 🙂

image

To understand how to implement your own URLProtocol, I propose to consider it on the example.

Put himself the task to realize a simple application with a list in which you have given the coordinates to display a list of snapshots cards. To download the snapshots will use the standard MKMapSnapshotter of MapKit, and loading data implement through a custom URLProtocol. The result might look something like this:

image

First, we write the data loading mechanism in the URL. To display a snapshot of the map we need to know the coordinates of the point — its latitude and longitude (latitude, longitude). Define the format of the custom URL, which I want to load the information:

map://?latitude=59.935634&longitude=30.325935

Now implement a URLProtocol, which will handle such links and generate the desired result. Create a class MapURLProtocolthat inherit from the base class URLProtocol. Despite its name, URLProtocol is though abstract but class. Do not fret, here we are using the other concepts — URLProtocol is the URLProtocol and the terms of the PLO is not relevant. So, MapURLProtocol:

class MapURLProtocol: URLProtocol {
}

Now override some mandatory methods, without which the URLProtocol will not work:

1. canInit(with:)

override class func canInit(with request: URLRequest) -> Bool { return request.url?.scheme == "map" }

Method canInit(with:) you need to specify the types of queries our URLProtocol can handle. For this example, assume that the Protocol will process only those requests to URL which shows the layout of the map. Before performing any query of the URL Loading System through all registered for the session protocols and calls this method. The first was the Protocol that this method will return true, and will be used to process the request.

2. canonicalRequest(for:)

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

Method canonicalRequest(for) is designed to bring the request to the canonical form. The documentation States that the Protocol implementation itself decides what constitutes a definition of this concept. Here you can normalize the schema, add headers to request, if necessary, etc. the Only requirement for this method for every incoming request should always be the same result, including the fact that this method is also used to search for cached answers to queries in URLCache.

3. startLoading()

In the method startLoading() describes all the logic for loading the required data. In this example, you need to parse the URL request and based on the values of its parameters latitude and longitude, refer to MKMapSnapshotter and download snapshot map.

override func startLoading() { guard let url = request.url let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else { fail(with: .badURL)
return
} load(with: queryItems)
} func load(with queryItems: [URLQueryItem]) { let snapshotter = MKMapSnapshotter(queryItems: queryItems)
snapshotter.start( with: DispatchQueue.global(qos: .background), completionHandler: handle
)
} func handle(snapshot: MKMapSnapshotter.Snapshot?, error: Error?) { if let snapshot = snapshot, let data = snapshot.image.jpegData(compressionQuality: 1) { complete(with data) } else if let error = error { fail(with error)
}
}

After receiving the data necessary to complete a job Protocol:

func complete(with data: Data) { guard let url = request.url, let client = client else {
return
} let response = URLResponse( url: url, mimeType: "image/jpeg", expectedContentLength: data.count textEncodingName: nil
) client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) client.urlProtocol(self, didLoad: data)
client.urlProtocolDidFinishLoading(self)
}

First of all create an object of type URLResponse. This object contains important metadata to answer the query. Then perform three important methods of object of type URLProtocolClient. Property client contains each entity URL-Protocol. It acts as a proxy between the URLProtocol and the entire URL Loading Systemthat when you call these methods makes the findings that have to do with the data: to cache, to pass to completionHandler request, to handle the completion of the Protocol Order and the number of calls to these methods may differ depending on the implementation of the Protocol. For example, we can load data from the network by batchas and periodically notify URLProtocolClientto show progress of data loading.

If an error occurs in the Protocol it is also necessary to correctly process and notify URLProtocolClient:

func fail(with error: Error) { client?.urlProtocol(self, didFailWithError: error)
}

This error is then sent to the completionHandler of the query, where it can be processed to show the beautiful message to the user.

4. stopLoading()

Method stopLoading() is called when the Protocol work for some reason, had been completed. It can be as successful completion and failure or cancel request. This is a good place to release resources held or to delete the temporary data.

override func stopLoading() { }

In this implementation of the URLProtocol is completed, it can be used anywhere in the application. To where to apply our Protocol, we add a few more things.

URLImageView

URLImageView class: UIImageView { var task: URLSessionDataTask? var taskId: Int? func render(url: URL) { assert(task == nil || task?.taskIdentifier != taskId) let request = URLRequest(url: url) task = session.dataTask(with: request completionHandler: complete) taskId = task?.taskIdentifier task?.resume()
} private func complete(data: Data?, response: URLResponse?, error: Error?) { if self.taskId == task?.taskIdentifier, let data = data, let image = UIImage(data: data) { didLoadRemote(image: image)
}
} func didLoadRemote(image: UIImage) { DispatchQueue.main.async { self.image = image
}
} func prepareForReuse() {
task?.cancel() taskId = nil image = nil
}
}

This is a simple class, heir to the UIImageView, like the implementation of which it is likely that any application you have. Here we are simply at the URL in the method render(url) load the image and write it to the property image. The convenience is that you can upload absolutely any images as http/https URLand our custom URL.

To query to download the images will also need an object of type URLSession:

let config: URLSessionConfiguration = { let c = URLSessionConfiguration.ephemeral c.protocolClasses = [
MapURLProtocol.self
] return c
}() let session = URLSession( configuration: config delegate: nil delegateQueue: nil
)

It is particularly important configuration session. In URLSessionConfiguration there is one important property — protocolClasses. Specifies a list of types URL-protocols session with this configuration can handle. The default session supports the processing of http/httpsprotocols, and if you want to support custom, you must specify them. For our example, we specify MapURLProtocol.

All that’s left to do is implement a View Controller that will display the picture cards. Its source code can be viewed here.

Here’s the result:

image

What about caching?

Everything works well except one important thing: when we scrollin list here and there, on the screen appear white spots. It seems that snapshots are cached and not on each call to render(url:) we re-load the data using MKMapSnapshotter. It takes time, and because such gaps in the download. We should implement the caching mechanism of the data to the already created snapshots not download again. Here we will use the power of the URL Loading System, which is already provided for this caching mechanism through URLCache.

Consider this process in more detail and divide the cache in two stages: reading and writing.

Reading

To correctly read the cached data, the URL Loading System to help get the answers to some important questions:

1. What URLCache use?
Of course, there are ready URLCache.shared, but the URL Loading System can’t always use it — because the developer might want to create and use your essence URLCache. To answer this question in the configuration session URLSessionConfiguration is the property urlCache. It is used for reading and writing responses to requests. Specify any URLCache for these purposes in our existing configuration.

let config: URLSessionConfiguration = { let c = URLSessionConfiguration.ephemeral c.urlCache = ImageURLCache.current c.protocolClasses = [
MapURLProtocol.self
] return c
}()

2. Whether to use cached data or perform the download again?
The answer to this question depends on request URLRequestthat we are going to perform. When you create a query, we have the opportunity in addition to the URL to specify the caching policy in the argument cachePolicy.

let request = URLRequest( url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30
)

The default value is used .useProtocolCachePolicy, this is also written in the documentation. This means that in this embodiment, the task of finding the cached response to the request and determining its relevance falls entirely on the implementation of the URLProtocol. But there is an easier way. If you set the value .returnCacheDataElseLoad, then when you create another entity URLProtocol URL Loading System will take part of the work itself: ask urlCache cached response for the current request using the method cachedResponse(for:). If cached data exists, then the object of type CachedURLResponse will be transferred immediately upon initialization URLProtocol and stored in the property cachedResponse:

override init( request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) {
super.init( request: request, cachedResponse: cachedResponse, client: client
)
}

CachedURLResponse is a simple class that contains the data (Data) and meta-information for them (URLResponse).
We can only slightly change the method startLoading and check inside it the value of this property and immediately terminate the Protocol work with these data:

override func startLoading() { if let cachedResponse = cachedResponse { complete(with: cachedResponse.data) } else { guard let url = request.url let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else { fail(with: .badURL)
return
} load(with: queryItems)
}
}

Entry

To find data in the cache, they are there presumably. This work also undertakes the URL Loading System. All that is required from us is to tell her that we want to cache the data at the completion of the Protocol using the policy setting caching cacheStoragePolicy. This is a simple enum with these values:

enum StoragePolicy { case allowed case allowedInMemoryOnly case notAllowed
}

They mean that caching is allowed in memory and on disk, only in memory or banned. In our example, we specify that caching is allowed in memory and on disk, because why not.

client.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed)

So, following a few simple steps, we supported the ability to cache snapshots cards. And now the application looks like this:

image

As you can see, no more white spots — the maps are downloaded once and then just pericolosa from the cache.

Not always simple

If you implement the URLProtocol, we encountered several falls.

The first was related to the internal implementation of the interaction of the URL Loading System with URLCache when caching answers to queries. The documentation indicatedthat despite the thread safety URLCache, work methods cachedResponse(for:) and storeCachedResponse(_:for:) to read / write the answers to the queries could lead to race conditions, so in subclasses URLCache should this point be taken into account. We expected that when using the URLCache.shared this problem will be solved, but it was not so. To fix this, we use a separate cache ImageURLCache, heir to the URLCachein which these methods are performed synchronously in a separate queue. As a nice bonus can separately from other entities URLCache to adjust the capacity of the cache in memory and on disk.

private static let accessQueue = DispatchQueue( label: "image-urlcache-access"
) override func cachedResponse(for request: URLRequest) -> CachedURLResponse? { return ImageURLCache.accessQueue.sync { return super.cachedResponse(for request)
}
} override func storeCachedResponse(_ response: CachedURLResponse, for request: URLRequest) { ImageURLCache.accessQueue.sync { super.storeCachedResponse(response for: request)
}
}

Another problem was reproduced only on devices with iOS 9. Methods to start and finish loading the URLProtocol can be run on different threads, which can lead to rare but nasty falls. To solve the problem, we store the current thread in the method startLoading and then code the download is complete, run directly on this thread.

var thread: Thread! override func startLoading() { guard let url = request.url let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems else { fail(with: .badURL)
return
} thread = Thread.current if let cachedResponse = cachedResponse { complete(with: cachedResponse) } else { load(request: request, url: url, queryItems: queryItems)
}
}
func handle(snapshot: MKMapSnapshotter.Snapshot?, error: Error?) { thread.execute { if let snapshot = snapshot, let data = snapshot.image.jpegData(compressionQuality: 0.7) { self.complete(with data) } else if let error = error { self.fail(with error)
}
}
}

When can we use a URL Protocol?

As a result, almost every user of our app for iOS one way or another, confronted with the elements, working via the URLProtocol. In addition to downloading media from the gallery, different implementations of URLprotocols help us to display maps and surveys, and also to show the avatars of conversations, compiled from photos of participants.

image

image

image

image

Like any decision, URLProtocol has its advantages and disadvantages.

Disadvantages URLProtocol

Lack of strong typing — when you create a URL scheme and options links are specified manually through the lines. If we mistype, we need the parameter will not be processed. This can complicate debugging the application and finding errors in his work. In Vkontakte we use special URLBuilder’s that form the final URL based on passed parameters. This solution is not very beautiful and somewhat defeats the purpose not to produce the additional effect, but the best idea yet. But we know that if you need to create a custom URL, then surely for him there is a special URLBuilder, which will help avoid mistakes.
Non-obvious fall — I have already described a couple of scenarios, because of which the application using the URLProtocol, may fall. There may be others. But such problems are as usual solved more thoughtful reading of the documentation, or in-depth study stack trace’and finding the root of the problem.

Advantages URLProtocol

Loose coupling of components — part of the application that need it initiates the download of data may not know how it is organized: what are the components used for this, how does the caching. We only know about the specific format of the URL and only interact with it.
Ease of implementation — to work correctly, the URLProtocol is sufficient to implement a few simple methods, and register the Protocol. After that it can be used anywhere in the application.
Ease of use — the application does not need to have additional data types that are involved in the data loading process, except for the URLProtocol. We are using already known types URL, URLSession, URLSessionDataTask.
Support caching — if done properly, the URLProtocol and configuration URL-session, as well as the correct query work on caching the data lies entirely with the URL Loading System.
*You can samokat API is a paragraph with an asterisk. If you want you can make it so that the queries will not be executed to the real API, and some own the cap that is implemented via the URLProtocol. It is possible to give any test data, so that even without access to this API to check the application status and depending on the answer, write the tests. At a certain point will only need to replace the use of URLProtocol with a custom scheme on standard http/https.

URLProtocol is not a panacea and may not be suitable for all tasks. It has advantages and disadvantages. But still, next time you need something to load for the given parameters, asynchronously to perform the operation, and the final data to cache, just to see if this approach will help to resolve the issue. Sometimes all it takes to solve the problem is the URL.

The full source code of the project can be found on our GitHub:
github.com/VKCOM/vk-ios-urlprotocol-example

Source