📜 ⬆️ ⬇️

Paparazzo. Powerful, stylish, your own. Part II


The first part of the story of the mediapicker Paparazzo

In the first part, we talked about how we came to our media pick-up and how many options we’ve touched before, and now it's time to continue the story.



Different sources of photos


The next challenge we faced was that the photos in MediaPicker could come from three different sources:


Of course, we wanted to have one entity, not three, so that in the code that works with photos, there was no need to make ugly branches, and to protect it from changes, if suddenly some new data source appears.
')
We have identified 4 actions that need to be performed when working with an image:


Well, of course, due to the fact that the image may not be available locally at the moment when we need it, the API for the first three points must be asynchronous. In order to display the image in the UI, without loading the memory with redundant data, you need to find out what size we need.

  1. To do this, you need to know the size of the area in which the display will take place and how we want to use it: whether we want to completely fit the image into it or we can sacrifice some of its parts so that there is no free space inside (similar to the content mode aspectFit and aspectFill in UIView).

  2. Since the API must be asynchronous, we need a handler in which we pass the resulting image to the UIImageView.

  3. It may also happen that we need to upload a photo from the network, but at the same time, we locally have a cached version of the same image, but smaller. And it turns out that if at boot time we substitute this reduced version into the view, the user will get the impression that the download is faster.

  4. Therefore, the deliveryMode parameter does not interfere either, putting down a progressive value, we kind of say that it is not against the bad versions of the requested picture, and the handler can be called several times as the quality increases. Best will mean that we want the handler to volunteer only once with the best version of the picture.

Accordingly, the method of requesting a picture with the listed parameters may look something like this.

func requestImage( viewSize: CGSize, contentMode: ContentMode, deliveryMode: DeliveryMode, handler: @escaping (UIImage?) -> ()) 

Reduce it by combining the first three parameters in the structure. This will allow us to add other parameters as needed without changing the method signature.

 func requestImage( options: ImageRequestOptions, handler: @escaping (UIImage?) -> ()) struct ImageRequestOptions { let viewSize: CGSize let contentMode: ContentMode let deliveryMode: DeliveryMode } 

The resulting version still needs work. First, in the handler clouge parameter, the UIImage type is explicitly specified, but we wanted to get rid of UIKit so that this method could be used not only on iOS.

Therefore, the UIImage must be replaced with something that can later be turned into a UIImage. There is a type that meets this criterion and is present on both iOS and macOS - this is a CGImage.

Therefore, we create the InitializableWithCGImage protocol.

 protocol InitializableWithCGImage { init(cgImage: CGImage) } 

By a happy coincidence, UIImage and NSImage already have such initializers, so all we have to do is add empty extensions for these classes, formally describing their compliance with this protocol.

 extension UIImage: InitializableWithCGImage {} extension NSImage: InitializableWithCGImage {} 

Replacing the UIImage with this protocol, we obtain such a method signature.

 func requestImage<T: InitializableWithCGImage>( options: ImageRequestOptions, handler: @escaping (T?) -> ()) 

Finally, care should be taken to ensure that the request can be canceled. To do this, add the ImageRequestId return value to the requestImage method, which will allow us to further identify the request.

 func requestImage<T: InitializableWithCGImage>( options: ImageRequestOptions, handler: @escaping (T?) -> ()) -> ImageRequestId 

There is one more small change.

I said earlier that if you set the deliveryMode value to progressive, the handler can be called several times. It would be nice inside this handler to understand whether it volunteered with the final or intermediate version of the image. Therefore, we will transfer to it the ImageRequestResult structure, which, in addition to the image itself, will contain other useful information about the result of the request.

 func requestImage<T: InitializableWithCGImage>( options: ImageRequestOptions, handler: @escaping (ImageRequestResult<T>) -> ()) -> ImageRequestId struct ImageRequestResult<T> { let image: T? let degraded: Bool let requestId: ImageRequestId } 

Thus, we have come to the final version of the image request method for displaying it in the interface.

Three other methods are simple, two of them are essentially just asynchronous getters.

 protocol ImageSource { func requestImage<T: InitializableWithCGImage>( options: ImageRequestOptions, resultHandler: @escaping (ImageRequestResult<T>) -> ()) -> ImageRequestId func fullResolutionImageData(completion: @escaping (Data?) -> ()) func imageSize(completion: @escaping (CGSize?) -> ()) func cancelRequest(_: ImageRequestId) } 

Thus, we obtained the ImageSource protocol, which is perfectly suitable for use as a model of our piker, and it only remains to implement it for three possible cases: photos from the disk, from the network and from the user's photo gallery.

Photo Gallery


Starting with iOS 8, access to the photo gallery is provided via Photos.framework. The gallery itself is directly represented in it by the PHPhotoLibrary object, and the photos by the PHAsset objects.



To get a photo view that can be displayed in the interface, you need to use PHImageManager, which gives a UIImage output.

The method that performs this conversion looks like this:

 func requestImage( for: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: @escaping (UIImage?, [AnyHashable: Any]?) -> ()) -> PHImageRequestID 

As you can see, it is very similar to the image acquisition method in our own ImageSource protocol: the same target size, content mode, some parameters, asynchronous result handler.

This is not surprising, since the first implementation of ImageSource was a wrapper over PHAsset, so we largely repelled from this signature.

Unfortunately, in the process of studying the work of PHImageManager, we encountered some slippery moments, so the body of our own requestImage method did not consist of a single call to this standard method, as it might seem.

The first of them manifested itself in solving the classical problem of displaying photos in a collection view.

  1. PHImageManager does not give any guarantees at all about how the resultHandler will be called after the cancellation of the request. It may or may not be called, but in some cases we will get some UIImage, and in some cases - nil instead of it. We wanted to simplify the client code so that it would not have to understand what exactly happened.

  2. Therefore, a strict set of call rules for resultHandler for ImageSource appeared, one of which stated that resultHandler should not be called after canceling the query.

The solution to this problem was quite simple. The resultHandler of PHImageManager is given as input two parameters: the first is a UIImage, and the second is an info dictionary, which contains all useful information.

 //  resultHandler PHImageManager' let cancelled = (info?[PHImageCancelledKey] as? NSNumber)?.boolValue ?? false || cancelledRequestIds.contains(requestId) if !cancelled { //  "̆" resultHandler } 

Among this information is a checkbox by which you can determine whether the request was canceled. But this flag may not come if the request was canceled after the given call to resultHandler was in the queue. Therefore, we had to keep within the ImageSource an array of canceled requestId, and check the presence of our request in it.

The second problem appeared when we encountered a photo from iCloud, and we needed to show the activity indicator at the time of loading.

The only way to track such downloads is to set the progress handler in the PHImageRequestOptions object, which is then passed to PHImageManager when the image is requested.

 class PHImageRequestOptions { //  PHImageManager var progressHandler: PHAssetImageProgressHandler? // ... } 

We only needed to track the fact of the start and end of the download, so we added two such closure to our own structure with the request parameters. And if onDownloadStart we just tugged at the first call of the progressHandler, then with onDownloadFinish it was not so simple.

 struct ImageRequestOptions { //  ImageSource var onDownloadStart: ((ImageRequestId) -> ())? var onDownloadFinish: ((ImageRequestId) -> ())? } 

If we were lucky, and progressHandler told us that the picture was 100% loaded, which corresponds to the value of progress == 1, we called onDownloadFinish in this place.

 phImageRequestOptions.progressHandler = { progress, _, _, _ in if progress == 1 { callOnDownloadFinish() } } 

However, the trick is that this may not happen, and the last call to the progressHandler will occur on progress less than 100%. In this case, we are forced to already inside resultHandler'a try to guess whether the download is completed or not.

 //  resultHandler: let degraded: Bool = info?[PHImageResultIsDegradedKey] let looksLikeLastCallback = cancelled || (image != nil && !degraded) if looksLikeLastCallback { callOnDownloadFinish() } 

In the info dictionary that comes to us in this callback, there is an IsDegraded flag that indicates whether we have received the final or intermediate version of the image. So at this stage it is logical to assume that the download is complete, either if we canceled the request, or if the final version of the picture arrived.

You can explore the implementation of the ImageSource for photos from disk and from the network in the Paparazzo repository .

Our mediipiker has already attracted the attention of iOS-developers, including foreign resources . They note that it perfectly performs the functions assigned to it and is quite elegant and simply implemented. Now you can freely try it, test it, discuss it. The Avito team is always happy to answer your questions.



Useful links:

Source: https://habr.com/ru/post/327486/


All Articles