All you need is URL

image






VKontakte users exchange 10 billion messages daily. They send each other photos, comics, memes and other attachments. Weโ€™ll tell you how in the iOS application we came up with uploading pictures using URLProtocol



, and step by step we will figure out how to implement our own.



About a year and a half ago, the development of a new message section in the VK application for iOS was in full swing. This is the first section written entirely in Swift. It is located in a separate module vkm



(VK Messages), which does not know anything about the device of the main application. It can even be run in a separate project - the basic functionality of reading and sending messages will continue to work. In the main application, message controllers are added via the corresponding Container View Controller to display, for example, a list of conversations or messages in a conversation.



Messages is one of the most popular sections of the VKontakte mobile application, so it is important that it works like a clock. In the messages



project, we fight for every line of code. We always really liked how neatly the messages are built into the application, and we strive to ensure that everything remains the same.



Gradually filling the section with new functions, we approached the following task: we had to make sure that the photo that is attached to the message was first displayed in a draft, and after sending it, in the general list of messages. We could just add a module to work with PHImageManager



, but additional conditions made the task more difficult.



image








When choosing a snapshot, the user can process it: apply a filter, rotate, crop, etc. In the VK application, this functionality is implemented in a separate AssetService



component. Now it was necessary to learn how to work with him from the message project.



Well, the task is quite simple, we will do it. This is approximately the average solution, because there are a lot of variations. We take the protocol, dump it in messages and start filling it with methods. We add to the AssetService, adapt the protocol and add our cache implementation! for viscosity. Then we put the implementation in messages, add it to some service or manager that will work with all this, and start using it. At the same time, a new developer still comes and, while trying to figure it all out, he condemns in a whisper ... (well, you understand). At the same time, sweat appears on his forehead.



image








This decision was not to our liking . New entities appear that message components need to know about when working with images from AssetService



. The developer also needs to do extra work to figure out how this system works. Finally, an additional implicit link appeared on the components of the main project, which we try to avoid so that the message section continues to work as an independent module.



I wanted to solve the problem so that the project didnโ€™t know anything at all about what kind of picture was chosen, how to store it, whether it needed special loading and rendering. At the same time, we already have the ability to download conventional images from the Internet, only they are not downloaded through an additional service, but simply by URL



. And, in fact, there is no difference between the two types of images. Just some are stored locally, while others are stored on the server.



So we came up with a very simple idea: what if local assets can also be learned to load via URL



? It seems that with one click of Thanosโ€™s fingers, it would solve all our problems: you donโ€™t need to know anything about AssetService



, add new data types and increase entropy in vain, learn to load a new type of image, take care of data caching. Sounds like a plan.



All we need is a URL



We considered this idea and decided to define the URL



format that we will use to load local assets:



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





We will use the value of the localIdentifier



property of localIdentifier



as the PHObject



, and we will pass the width



and height



parameters to load the images of the desired size. We also add some more parameters like crop



, filter



, rotate



, which will allow you to work with the information of the processed image.



To handle these URL



we will create an AssetURLProtocol



:



 class AssetURLProtocol: URLProtocol { }
      
      





Its task is to load the image through AssetService



and return back the data that is already ready for use.



All this will allow us to almost completely delegate the work of the URL



protocol and URL Loading System



.



Inside the messages it will be possible to operate with the most common URL



, only in a different format. It will also be possible to reuse the existing mechanism for loading images, it is very simple to serialize in the database, and implement data caching through standard URLCache



.



Did it work out? If, reading this article, you can attach a photo from the gallery to the message in the VKontakte application, then yes :)



image






To make it clear how to implement your URLProtocol



, I propose to consider this with an example.



We set ourselves the task: to implement a simple application with a list in which you need to display a list of map snapshots at the given coordinates. To download snapshots, we will use the standard MKMapSnapshotter



from MapKit



, and we will load data through the custom URLProtocol



. The result might look something like this:



image






First, we implement the mechanism for loading data by URL



. To display the map snapshot, we need to know the coordinates of the point - its latitude and longitude ( latitude



, longitude



). Define the custom URL



format by which we want to load the information:



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





Now we implement URLProtocol



, which will process such links and generate the desired result. Let's create the MapURLProtocol



class, which we will inherit from the base class URLProtocol



. Despite its name, URLProtocol



is, although abstract, but a class. Donโ€™t be embarrassed, here we use other concepts - URLProtocol



represents exactly the URL



protocol and has no relation to OOP terms. So MapURLProtocol



:



 class MapURLProtocol: URLProtocol { }
      
      





Now we redefine some required methods without which the URL



protocol will not work:



1. canInit(with:)





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





The canInit(with:)



method is needed to indicate what types of requests our URL



protocol can handle. For this example, suppose that the protocol will only process requests with a map



scheme in the URL



. Before starting any request, the URL Loading System



goes through all the protocols registered for the session and calls this method. The first registered protocol, which in this method will return true



, will be used to process the request.



canonicalRequest(for:)





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





The canonicalRequest(for:)



method is intended to reduce the request to canonical form. The documentation says that the implementation of the protocol itself decides what to consider as the definition of this concept. Here you can normalize the scheme, add headers to the request, if necessary, etc. The only requirement for this method to work is that for every incoming request there should always be the same result, including because this method is also used to search for cached answers requests in URLCache



.



3. startLoading()





The startLoading()



method describes all the logic for loading the necessary data. In this example, you need to parse the request URL



and, based on the values โ€‹โ€‹of its latitude



and longitude



parameters, turn to MKMapSnapshotter



and load the desired map snapshot.



 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, it is necessary to correctly shut down the 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 for responding to a request. Then we execute three important methods for an object of type URLProtocolClient



. The client



property of this type contains each entity of the URL



protocol. It acts as a proxy between the URL



protocol and the entire URL Loading System



, which, when these methods are called, draws conclusions about what needs to be done with the data: cache, send requests to completionHandler



, somehow process the protocol shutdown, etc. and the number of calls to these methods may vary depending on the protocol implementation. For example, we can download data from the network with batches and periodically notify URLProtocolClient



about this in order to show the progress of data loading in the interface.



If an error occurs in the protocol operation, it is also necessary to correctly process and notify URLProtocolClient



about this:



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





It is this error that will then be sent to the completionHandler



the request execution, where it can be processed and a beautiful message displayed to the user.



4. stopLoading()





The stopLoading()



method is called when the protocol has been completed for some reason. This can be either a successful completion, or an error completion or a request cancellation. This is a good place to free up occupied resources or delete temporary data.



 override func stopLoading() { }
      
      





This completes the implementation of the URL



protocol; it can be used anywhere in the application. To be where to apply our protocol, add a couple more things.



URLImageView





 class URLImageView: 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, the descendant of UIImageView



, a similar implementation of which you probably have in any application. Here we simply load the image by the URL



in the render(url:)



method and write it to the image



property. The convenience is that you can upload absolutely any image, either by http



/ https



URL



, or by our custom URL



.



To execute requests for loading images, you 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 )
      
      





Session configuration is especially important here. In URLSessionConfiguration



there is one important property for us - protocolClasses



. This is a list of the types of URL



protocols that a session with this configuration can handle. By default, the session supports processing of http



/ https



protocols, and if custom support is required, they must be specified. For our example, specify MapURLProtocol



.



All that remains to be done is to implement the View Controller, which will display map snapshots. Its source code can be found here .



Here is the result:



image






What about caching?



Everything seems to work well - except for one important point: when we scroll the list back and forth, white spots appear on the screen. It seems that snapshots are not cached in any way and for each call to the render(url:)



method, we MKMapSnapshotter



data through MKMapSnapshotter



. This takes time, and therefore such gaps in loading. It is worth implementing a data caching mechanism so that already created snapshots are not downloaded again. Here we will take advantage of the power of the URL Loading System



, which already has a caching mechanism for URLCache



provided for this.



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



Reading



In order to correctly read cached data, the URL Loading System



needs to be helped to get answers to several important questions:



1. What URLCache to use?



Of course, there is already finished URLCache.shared



, but the URL Loading System



cannot always use it - after all, the developer may want to create and use his own URLCache



entity. To answer this question, the URLSessionConfiguration



session URLSessionConfiguration



has a urlCache



property. It is used for both reading and recording responses to requests. We will URLCache



some 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. Do I need to use cached data or download again?



The answer to this question depends on the URLRequest



request we are about to execute. When creating a request, we have the opportunity to specify a cache policy in the cachePolicy



argument in addition to the URL



.



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





The default value is .useProtocolCachePolicy



; this is also written in the documentation. This means that in this option, the task of finding a cached response to a request and determining its relevance lies entirely with the implementation of the URL



protocol. But there is an easier way. If you set the value .returnCacheDataElseLoad



, then when creating the next entity URLProtocol



URL Loading System



will take on some of the work: it will ask urlCache



cached response to the current request using the cachedResponse(for:)



method. If there is cached data, then an object of type CachedURLResponse



will be transferred immediately upon initialization of the URLProtocol



and stored in the cachedResponse



property:



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





CachedURLResponse



is a simple class that contains data ( Data



) and meta-information for them ( URLResponse



).



We can only change the startLoading



method a startLoading



and check the value of this property inside it - and immediately end the protocol with this 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) } }
      
      





Record



To find data in the cache, you need to put it there. The URL Loading System



also takes care of this work. All that is required of us is to tell her that we want to cache the data when the protocol cacheStoragePolicy



using the cacheStoragePolicy



cache policy cacheStoragePolicy



. This is a simple enumeration with the following values:



 enum StoragePolicy { case allowed case allowedInMemoryOnly case notAllowed }
      
      





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



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





So, by following a few simple steps, we supported the ability to cache map snapshots. And now the applicationโ€™s work looks like this:



image






As you can see, there are no more white spots - the cards are loaded once and then simply reused from the cache.



Not always easy



When implementing the URL



protocol, we encountered a series of crashes.



The first was related to the internal implementation of the interaction of the URL Loading System



with URLCache



when caching responses to requests. The documentation states : despite the URLCache



safety of URLCache



, the operation of the cachedResponse(for:)



and storeCachedResponse(_:for:)



methods for reading / writing responses to requests can lead to a race of states, therefore, this point should be taken into account in URLCache



subclasses. We expected that using URLCache.shared



this problem would be solved, but it turned out to be wrong. To fix this, we use a separate ImageURLCache



cache, a descendant of URLCache



, in which we execute the specified methods synchronously on a separate queue. As a pleasant bonus, we can separately configure the cache capacity in memory and on the disk separately from other URLCache



entities.



 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. The methods for starting and ending the loading of the URL



protocol can be performed on different threads, which can lead to rare but unpleasant crashes. To solve the problem, we save the current thread in the startLoading



method and then execute the download completion code 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 a URL protocol come in handy?



As a result, almost every user of our iOS application in one way or another encounters elements that work through the URL



protocol. In addition to downloading media from the gallery, various implementations of URL



protocols help us display maps and polls, as well as show chat avatars composed of photos of their participants.



image






image






image






image






Like any solution, URLProtocol



has its advantages and disadvantages.



Disadvantages of URLProtocol







URLProtocol





URL



- โ€” . . - , - , , , โ€” , . , , โ€” URL



.



GitHub



All Articles