📜 ⬆️ ⬇️

Drag & Drop in your iOS apps



The Drag & Drop mechanism, working in iOS 11 and iOS 12 , is a way to graphically asynchronously copy or move data both within one application and between different applications. Although this technology is about 30 years old, it has literally become a “breakthrough” technology on iOS due to the fact that when dragging and dropping something in iOS , multitouch allows you to freely interact with the rest of the system and collect data for resetting from different applications.

iOS makes it possible to capture multiple items at once. Moreover, they do not have to be easily accessible for selection: you can take the first object, then go to another application and grab something else - all the objects will be collected in a “stack” under your finger. Then call the universal dock on the screen, open any application there and capture the third object, then go to the screen with applications running and, without releasing the objects, drop them into one of the open programs. Such freedom of action is possible on the iPad , on the iPhone , the range of Drag & Drop in iOS limited to the scope of one application.

Most popular applications ( Safary , Chrome , IbisPaint X , Mail , Photos , Files , etc.) already have Drag & Drop . In addition, Apple provided developers with a very simple and intuitive API for embedding the Drag & Drop mechanism in your application. The Drag & Drop mechanism, like the gestures, works on UIView and uses the concept of “interactions” Interactions , which are a bit like gestures, so you can think of the Drag & Drop mechanism as just a really powerful gesture.
')
It, as well as gestures, is very easy to integrate into your application. Especially if your application uses the UITableView table or the UICollectionView collection, since for them API Drag & Drop improved and raised to a higher level of abstraction in the sense that the Collection View itself helps you with the indexPath of the collection item that you want to drag. Drag . It knows where your finger is and interprets it as the indexPath of a collection item that you are dragging Drag at the moment or as the indexPath of a collection item where you Drop something Drop . So the Collection View collection provides you with indexPath , but otherwise it’s absolutely the same API Drag & Drop as for a regular UIView .

The Drag & Drop process on iOS has 4 different phases:

Lift


Lift is when the user performs a long press gesture, indicating an element that will be “dragged and dropped.” At this moment, a very lightweight so-called “preview” ( lift preview ) of the specified element is formed, and then the user begins to drag ( Dragging ) his fingers.



Drag (dragging)


Drag (dragging) - this is when the user moves the object along the surface of the screen. During this phase, a “preview” ( lift preview ) for this object can be modified (a green plus sign "+" or another sign appears) ...



... some interaction with the system is also allowed: you can click on some other object and add it to the current “drag and drop” session:



Drop


Drop occurs when the user lifts a finger. At this point, two things can happen: either the Drag object will be destroyed, or the Drop object will be reset at the destination.



Data Transfer


If the Drag drag and drop process was not canceled and Drop was “reset”, then Data Transfer (data transfer) occurs, at which the “reset point” requests data from the “source”, and asynchronous data transfer occurs.

In this tutorial, using the Picture Gallery demo application borrowed from homework from the Stanford CS193P course , we will show how easy it is to embed the Drag & Drop mechanism into your iOS app.
We will endow the Collection View collection with the ability to fill ourselves with images OUTSIDE, as well as reorganize the elements INSIDE yourself using the Drag & Drop mechanism. In addition, this mechanism will be used to reset unnecessary elements of the Collection View into a “trash can”, which is a common UIView and is represented by a button on the navigation bar. We can also share images from other applications, such as Notes or Notability or Mail or the Photo Library ( Photo ), using the Drag & Drop mechanism.

But before focusing on the implementation of the Drag & Drop mechanism in the demo application “Gallery of Images”, I will briefly go through its main components.

Features of the “Image Gallery” demo application


The user interface ( UI ) of the Image Gallery application is very simple. This is the “screen capture” Image Gallery Collection View Controller inserted into the Navigation Controller :



The central part of the application is of course the Image Gallery Collection View Controller , which is supported by the ImageGalleryCollectionViewController class with the Image Gallery Model as a variable var imageGallery = ImageGallery () :



The model is represented by a struct ImageGallery containing an array of images images , in which each image is described by a struct ImageModel structure that contains the URL url of the image location (we are not going to store the image itself) and its aspect ratio aspectRatio :



Our ImageGalleryCollectionViewController implements the DataSource protocol:



The user cell of the cell collection contains the image imageView: UIImageView! and spinner activity indicator : UIActivityIndicatorView! and is supported by the custom subclass ImageCollectionViewCell of the UICollectionViewCell class:



Public API of the ImageCollectionViewCell class is the imageURL image URL . As soon as we install it, our UI updated, that is, the data for the image on this imageURL is selected asynchronously and displayed in a cell. While data is being sampled from the network, the spinner activity indicator is working, indicating that we are in the process of retrieving data.

I use the global global queue (qos: .userInitiated) with the qos argument “quality of service”, which is set to .uInIniatediated , because I select data at the user's request:



Every time you use your own variables inside the closure, in our case it is imageView and imageURL , the compiler forces you to set self. so you ask yourself: “Isn’t there a“ cyclic memory reference ”( memory cycle )?” We don’t have an explicit “cycling memory reference” ( memory cycle ), because self itself does not have a pointer to this closure.

However, in the case of multithreading, you must take into account that the cells in the Collection View collection are reusable thanks to the dequeueReusableCell method. Each time a cell (new or reused) hits the screen, the image is launched asynchronously from the network (the spinner of the spinner activity indicator is spinning at this time).

Once the download is complete and the image is received, the UI this collection cell is updated. But we don’t wait for the image to load, we continue to scroll through the collection and the collection cell that we’ve noticed goes off the screen without updating our UI . However, a new image should appear from below, and the same cell that has left the screen will be reused, but for another image, which may load quickly and update the UI . At this time, the image download previously launched in this cell will return and the screen will be updated, which will lead to an incorrect result. This is because we are running different things working on the network in different threads. They return at different times.

How can we fix the situation?
Within the framework of the GCD mechanism we use, we cannot cancel the loading of the image of the cell leaving the screen, but we can, when our imageData data come from the network, check the URL url that caused the download of this data and compare it with what the user wants to have in this cell at the moment, that is, imageURL . If they do not match, then we will not update the UI cell and wait for the image data we need:



This seemingly absurd line of code url == self.imageURL makes everything work properly in a multi-threaded environment that requires non-standard imagination. The fact is that some things in multithreaded programming occur in a different order than the written code.

If the image data could not be sampled, an image is generated with an error message in the form of an “Error” string and an Emodji with a “frowning face”. Just the empty space in our Collection View can confuse the user a bit:



We would not like the image with the error message to repeat the aspectRatio of this erroneous image, because in this case the text along with the emoji will stretch or shrink. We would like it to be neutral - square, that is, it would have an aspect ratio of aspectRatio close to 1.0.



We must report this request to our Controller so that it corrects the aspect ratio of its aspectRatio for the corresponding indexPath in its ImageGallery model . This is an interesting task, there are many ways to solve it, and we will choose the easiest of them - using Optional closure var changeAspectRatio: (() -> Void)? . It can be nil and need not be installed if there is no need for it:



When I call the closure changeAspectRatio? () In the case of an erroneous sample of data, I use the Optional chain. Now anyone who is interested in some kind of setting when receiving an erroneous image can set this closure to something specific. And that is what we do in our Controller in the cellForItemAt method:



Details can be found here .

To display images with the correct aspectRatio, use the sizeForItemAt delegate's UICollectionViewDelegateFlowLayout method :



In addition to the Collection View image Collection View , on our UI we placed the Bar Button with a custom GarbageView image, containing the “trash can” as a subview on the navigation bar:



In this picture, the background colors for the GarbageView itself and the UIButton buttons with the image of a "trash can" (actually there is a transparent background) are specially changed so that you can see that the user who "dumps" the Gallery images into the "trash can", Much more room to maneuver when “dropping” Drop than just a “trash can” icon.
The GarbageView class has two initializers and both use the setup () method:



In the setup () method, I also add the myButton button as a subview with the image of a "trash can" taken from the standard Bar Button Trash button:



I set the transparent background for the GarbageView :



The size of the garbage bin and its location will be determined in the layoutSubviews () method of the UIView class, depending on the bounds of this UIView :



This is the initial version of the “Image Gallery” demo, and is located on Github in the ImageGallery_beginning folder. If you launch this version of the Image Gallery application, you will see the result of the application’s work on the test data, which we will subsequently delete and fill in the Image Gallery only OTHER:



The plan for implementing the Drag & Drop mechanism in our application is as follows:

  1. First, we will give our collection of Images of the Collection View ability to drag Drag UIImage from outside both locally and from it,
  2. then we will teach our collection of Collection View images to take drag-drawn Drag from outside or locally UIImage images,
  3. we will also teach our GarbageView with the “trash can” button to take “Upper” images from the local Collection View and remove them from the Collection View collection.


If you go through to the end of this tutorial and make all the necessary code changes, you will receive the final version of the “Image Gallery” demo application, which has Drag & Drop mechanism implemented. It is located on Github in the ImageGallery_finished folder.

The efficiency of the Drag & Drop mechanism in your Collection View ensured by two new delegates.
The first delegate's methods , dragDelegate , are configured to initialize and customize the drag-and-drop Drags .
The methods of the <u second delegate, dropDelegate , complete the Drag-and-Drop drags and, basically, provide data transfer ( Data transfer ) and custom animation settings for Drop , as well as other similar things.

It is important to note that both of these protocols are completely independent. You can use one or another protocol if you need only Drag dragging or Drop , but you can use both protocols at the same time and drag Drag and drop Drop , which opens up additional functionality. Drag & Drop mechanism for changing the order of elements in your Collection View .

Dragging Drag Elements From Collection View


Implementing the Drag protocol is very simple, and the first thing you should always do is install yourself, self , as a delegate to dragDelegate :



And, of course, at the very top of the ImageGalleryCollectionViewController class , you should say that “Yes”, we implement the UICollectionViewDragDelegate protocol:



As soon as we do this, the compiler starts to “complain”, we click on the red circle and we are asked: “Do you want to add the mandatory methods of the UICollectionViewDragDelegate protocol?”
I answer: “Of course I want!” And click on the Fix button:



The only mandatory method of the UICollectionViewDragDelegate protocol is the itemsForBeginning method, which tells Drag system WHAT we are dragging. The itemsForBeginning method is called when the user starts dragging a cell of the cell collection.

Notice that the Collection View added an indexPath to this method. This will tell us which element of the collection, which indexPath , we are going to “drag”. This is really very convenient for us, since it is up to the application to take responsibility for using the session and indexPath arguments to figure out how to handle this Drag dragging.

If the [UIDragItems] array of the “overtightened ” elements is returned, Drag dragging is initialized; if the empty array [] is returned, the Drag dragging is ignored.

I will create a small private function dragItems (at: indexPath) with an argument indexPath . It returns the array we need [UIDragItem] .



What does a drag-and-drop UIDragItem look like ?
It has only one very IMPORTANT thing called itemProvider . itemProvider is just something that can provide data that will be dragged.

And you have the right to ask: “What about the“ dragging ”of an UIDragItem item that simply has no data?” The item that you want to drag may not have data, for example, because creating this data is a costly operation. This may be an image or something that requires downloading data from the Internet. Remarkably, the Drag & Drop operation is completely asynchronous. When you start Drag dragging, it’s really a very lightweight object ( lift preview ), you drag it everywhere, and nothing happens during that dragging. But as soon as you “drop” Drop your object somewhere, then being an ItemProvider , you really should supply your “dragged” and “thrown” object with real data, even if it takes a certain amount of time.

Fortunately, there are many built-in itemProviders . These are classes that already exist in iOS and that are itemPoviders , such as NSString , for example, which allows you to drag text without fonts. Of course, this is a UIImage image. You can select and drag UIImages everywhere. NSURL class, which is absolutely wonderful. You can go to the Web page, select the URL and “throw” it wherever you want. This may be a link to the article or the URL for the image, as it will be in our demo. These are the color classes of UIColor , the MKMapItem map element , the CNContact contact from the address book, a lot of things you can select and drag. All of them are itemProviders .

We're going to drag a UIImage image. It is in the Collection View cell with the indexPath , which helps me to select a cell , get an Outlet imageView from it and get its image .

Let's express this idea in a couple of lines of code.
First, I query my Collection View about the cell cell for the item element corresponding to this indexPath .



The cellForItem method (at: IndexPath) for the Collection View only works for visible ( visible ) cells, but, of course, it will work in our case, because I’m dragging the Drag item on the screen and it is visible.

So, I received a "draggable" cell cell .
Next, I use the as operator ? to this cell so that it has the TYPE of my custom subclass . And if it works, then I get an Outlet imageView , from which I take its image . I just captured the image image for this indexPath .

Now that I have an image , all I need to do is create one of these UIDragItems , using the resulting image image as itemProvider , that is, the thing that provides us with data.
I can create dragItem with the UIDragItem constructor, which takes as an argument itemProvider :



Then we create an itemProvider for the image image also using the constructor NSItemProvider . There are several constructors for NSItemProvider , but among them there is one really remarkable - NSItemProvider (object: NSItemProviderWriting) :



To this constructor NSItemProvider, you simply give an object an object , and it knows how to make an itemProvider out of it. As such an object, I give the image the image image that I received from the cell of the cell and get the itemProvider for the UIImage .
And it's all. We created dragItem and should return it as an array having one element.

But before I return the dragItem , I'm going to do one more thing, namely, set the localObject variable for the dragItem equal to the resulting image image .



What does this mean?
If you drag and drop Draglocally, that is, inside your application, then you do not need to go through all this code associated with itemProvider , through asynchronous data retrieval. You do not need to do this, you just need to take localObject and use it. This is a kind of “short circuit” with a local “drag and drop” Drag.

The code we write will work when “dragging”Dragbeyond our collection Collection Viewto other applications, but if we “drag and drop” Draglocally, we can use localObject . Next, I return an array consisting of a single dragItem element .

Incidentally, if I could not get for some reason image for this cell cell , I return empty array [] , this means that the "drag" Dragis canceled.



In addition to the local object localObject , you can remember the local context localContext for our Dragsession session . In our case, it will be a collectionView collection.and it is useful to us afterwards:



Having "drag and drop" Drag, you can add more items items to this "drag and drop", just doing the gesture tap on them. As a result, you can drag a Draglot of items at once. And it is easy to implement using another method delegate UICollectionViewDragDelegate , very similar to the method itemsForVeginning , a method called itemsForAddingTo . The itemsForAddingTo method looks exactly the same as the itemsForVeginning method , and returns exactly the same thing, because it also gives us the indexPathof what the user “tapped” on during the “drag Dragand drop” process , and it is enough for me to get an image from the cell where the user “tapped” and return it.



Returning an empty [] array from the itemsForAddingTo method causes the tap gesture to be interpreted in the usual way, that is, as the choice of this cell .
And that’s all we need to drag and drop Drag.
Run the application.
I select the “Venice” image, hold it for a while and start moving ...



... and we can actually drag this image into the appPhotos, as you see a green plus sign "+" in the upper left corner of the "drag and drop" image. I can perform a tap gesture on one more image of "Artik" from the collection Collection View...



... and now we can already throw two images into the application Photos:



Since the application Photoshas a mechanism already built in Drag & Drop, everything works fine, and this is cool.
So, I have to “overtighten” Dragand “reset” the DropGallery image to other applications, I did not have to do much in my application, except for the delivery of the image as an array [UIDragItem] . This is one of the many great features of the mechanism.Drag & Drop - it is very easy to make it work in both directions.

Reset of Dropimages In a collectionCollection View


Now we need to make a Droppart for my collection Collection Viewso that we can “drop” Dropany “draggable” images INSIDE of this collection. “Dragged” image can “come” both from outside and directly from inside of this collection.
To do this, we do the same thing done to delegate dragDelegate , ie make ourselves, the self , delegate dropDelegate in the method of the viewDidLoad :



Again, we have to climb to the top of our class ImageGalleryCollectionViewController and verify protocol implementation UICollectionViewDropDelegate :



As soon as we added our new protocol, the compiler again began to “complain” that we did not implement this protocol. We click on the button Fix, and before us appear the mandatory methods of this protocol. In this case, we are told that we must implement the performDrop method :



We must do this, otherwise a “reset” will not occur Drop. In fact, I'm going to implement the performDrop method last, because there are a couple of other highly recommended Applemethods that need to be implemented for the Droppart. These are canHandle and dropSessionDidUpdate :



If we implement these two methods, then we can get a small green plus sign “+” when we drag images OUTSIDE onto our collection ollection View, and besides, we will not try to dump something we do not understand.

Let's implement canHandle . You have a version of the canHandle method that is intended for the collection ollection View. But this method ollection Viewlooks exactly the same as a similar method for a regular UIView , there is no indexPath . We just need to return session.canLoadObjects (ofClass: UIImage.self) , which means that I accept the “reset” of the objects of this class PAS in my collection ollection View:



But this is not enough to “dump” the Dropimage into my collection Collection ViewOUTSIDE.
If an Dropimage is dumped INSIDE a collection Collection View, when a user reorganizes his own elements of items using a mechanism Drag & Drop, then a single UIImage image is enough , and the implementation of the canHandle method will look like this.

But if the Dropimage “dumping” happens EXTERNAL, then we need to process only those “drag and drop” Dragthat represent the UIImage image along with URLfor this image, since we are not going to store the UIImage images directlyin the model. In this case, I will return true in the canHandle method only if a pair of session.canLoadObjects conditions (ofClass: NSURL.self) && session.canLoadObjects (ofClass: UIImage.self) are simultaneously executed & :



it remains for me to determine whether I am dealing with a "reset" From outside or inside. I'll do it with the help of calculated constants isself , for the calculation of which I can use such a thing in Dropthe session the session , both its local Dragsession localDragSession . This local Dragsession in turn has a local context localContext .
If you remember, we set this local context in the methoditemsForVeginning Drag delegate UICollectionViewDragDelegate :



I will examine the local context of localContext for equality to my collectionView . True TYPE for localContext will be Any , and I need to make a "casting" of TYPE Any with the help of the as operator ? UICollectionView :



If the local context (session.localDragSession? .LocalContext as? UICollectionView) is equal to my collectionView , then the calculated variable isSelf is trueand there is a local "reset" INSIDE my collection. If this equality is violated, then we are dealing with a "reset" DropEXTERNAL.

The canHandle method reports that we can only handle this kind of “drag and drop” Dragonto our collection Collection View. Otherwise, it doesn’t make sense at all to talk about “dumping” Drop.

If we continue “resetting” Drop, then even before the user lifts his fingers from the screen and a real “reset” occurs Drop, we must inform the delegate UICollectionViewDropDelegate about the UIDropProposal proposal to perform a reset iOSusing the dropSessionDidUpdate method .Drop

In this method, we must return a Dropclause that can have the values .copy or .move or .cancel or .forbidden for the operation argument . And these are all the possibilities that we have in the usual case when dealing with the usual UIView .

But the collection Collection Viewgoes further and offers to return the specialized UICollectionViewDropProposal clause , which is subclassof the UIDropProposal class and allows, in addition to the operation operation, to specify an additional intent parameter for the collection Collection View.

ParameterThe intent tells the collectionCollection Viewwhether we want the “reset” element to be placed inside an already existing cell or whether we want to add a new cell. See the difference? In the case of a collection,Collection Viewwe must report our intent intent .

In our case, we always want to add a new cell, so you will see what our intent parameter is equal to.
Select the second constructor for the UICollectionViewDropProposal :



In our case, we always want to add a new cell and the intent parameterwill take the value .insertAtDestinationIndexPath as opposed to.insertIntoDestinationIndexPath .



I again used the calculated isSelf constant, and if this is a self- reorganization, then I perform a .move move, otherwise I do a copy of .copy . In both cases, we use .insertAtDestinationIndexPath , that is, inserting new cells .

So far I have not implemented the performDrop method, but let's take a look at what the collection can already doCollection Viewwith this small piece of information that we provided to it.

I am dragging an image outSafariwith a search engineGoogle, and this image has a green "+" sign on top, indicating that our Gallery of Images is ready not only to receive and copy this image along with it URL, but also to provide a place inside the collection Collection View:



I can click on a couple more images in Safari, and “Dragged” images will already be 3:



But if I lift my finger and “drop” Dropthese images, they will not be placed in our Gallery, but will simply return to their former places, because we have not yet implemented the performDrop method .



You could see that the collection Collection Viewalready knows what I want to do.
The collection Collection Viewis absolutely wonderful thing for the mechanism.Drag & Drop, it has very powerful functionality for this. We barely touched it, having written 4 lines of code, and it already advanced far enough in the perception of “reset” Drop.
Let's go back to the code and implement the performDrop method .



In this method, we will not be able to do with 4 lines of code, because the performDrop method is a bit more complicated, but not too complicated.
When a “reset” occurs Drop, then in the performDrop method we have to update our Model, which is imageGallery Image Gallery with a list of images images , and we need to update our collectionView visual collection .

We have two different “reset” scenarios Drop.

If there is a “reset” Dropfrom my collectionView , then I need to “reset” the Dropcollection item at the new location and remove it from the old location, because in this case I move ( .move ) this collection item. This is a trivial task. If

there is a “reset” Dropfrom another application, we must use the itemProvider property of the “drag and drop” item element to select data.

When we perform a “reset” Dropin the collectionView collection , the collection provides us with the coordinator coordinator. The first and most important thing that the coordinator coordinator tells us is the destinationIndexPath , that is , the destination-point indexPath of the “reset” Drop, that is, where we will be “reset”.



But destinationIndexPath can be equal to nil , since you can drag the “dumped” image to a part of the collection Collection Viewthat is not between any existing cells , so that it may well be nil . If this is the situation, then I create an IndexPath with the 0th item element in the 0th section of the section .



I could choose any other indexPath , but this indexPath I will use by default.

Now we know where we are going to “dump” Drop. We must go through all the “reset” elements of coordinator.items provided by the coordinator coordinator . Each item from this list has the UICollectionViewDropItem TYPE and can provide us with very interesting pieces of information.

For example, if I can get the sourceIndexPath from item.sourceIndexPath , then I’ll know for sure that this “drag and drop” Dragis done from myself, self, and the source of the drag Dragis the collection item with the indexPath equal to sourceIndexPath :



I don’t even have to look at localContext in this case to find out that this “drag and drop” was done INSIDE the collectionView collection . Great!

Now I know the sourceIndexPath source and destination-point destinationIndexPath Drag & Drop , and the task becomes trivial. All I need to do is update the Model so that the source and destination point are swapped, and then update the collectionView collection to remove the collection item from sourceIndexPath and add it to the collection from destinationIndexPath .

Our local case is the simplest one, because in this case the mechanism Drag & Dropworks not just in the same application, but also in the same collectionView collection , and I can get all the necessary information using the coordinator coordinator. Let's implement this simplest local case:



In our case, I don’t even need the localObject , which I “hid” earlier when I created the dragItem and which I can now borrow from the “drag and drop” item in the item collection in the form item.localObject . We will need it when “dumping” Dropimages into the “trash can”, which is in the same application, but is not the same collectionView collection . Now two IndexPathes are enough for me : the source sourceIndexPath and the destination point destinationIndexPath .

I get the information firstimageInfo about the image in the old place from the Model, removing it from there. And then insert into an array of images of my models imageGallery information imageInfo an image with a new index destinationIndexPath.item . This is how I updated my Model:



Now I have to update the collection collection itself. It is important to understand that I do not want to overload all the data in my collection collectionView using reloadData () in the middle of the "drag and drop" processDrag, because it resets the whole "world" of our gallery of images, which is very bad, I do not. Instead, I'm going to remove and insert items.items separately:



I deleted the item in a collection collectionView with sourceIndexPath and insert a new item in the collection with destinationIndexPath .

It looks as though this code works fine, but in reality, this code may “crash” your application. The reason is that you make numerous changes to your collectionView collection , and in this case, each step of the collection change must be normally synchronized with the Model, which in our case is not respected, since we perform both operations at the same time: delete and insert. Therefore, collection collectionViewwill be at some point in a non-synchronized state with the Model.

But there is a really cool way to get around this, which is that the collection view collection has a method called performBatchUpdates that has a closure ( closure) and within that closure I can place any number of these deleteItems , insertItems , moveItems and all I want:



Now, deleteItems and insertItems will be executed as a single operation, and there will never be a lack of synchronization of your Model with the collectionView collection .

And finally, the last thing we need to do is to ask the coordinator coordinator to perform and animate the “reset” itself Drop:



As soon as you lift your finger off the screen, the image moves, everything happens at the same time: “reset”, the image disappears in one place and appearance in another.
Let's try to move the “Venice” test image in our Image Gallery to the end of the first row ...



... and “reset” it:



As we wanted, it was placed at the end of the first row.
Hooray! Everything is working!

Now let us deal NOT with the local case, that is, when the “reset” element comes OUTSIDE, that is, from another application.
To do this, we write else in the code with respect to sourceIndexPath . If we do not have sourceIndexPath , then this means that the “reset” item came from somewhere OUTSIDE and we will have to enable data transfer using the itemProver reset item item.dragItem.itemProvider element :



If you “drag Dragand drop” EITHER and “drop” "DropDoes this information become available instantly? No, you select data from the “dragged” thing ASYNCHRONOUS. But what if the sample takes 10 seconds? What will the collection do at this time ollection View? In addition, the data may not come at all in the order in which we requested it. To manage this is not at all easy, and in this case it Appleproposed a ollection Viewcompletely new technology for using substitutes Placeholders.

You place a Collection Viewsubstitute in your collection Placeholder, and the collection Collection Viewmanages it all for you, so all you have to do when the data is finally selected is to ask the substitute to Placeholdercall its context placeholderContextand inform him that you received the information. Then update your Model and the context of the placeholderContext AUTOMATICALLY swap a cell cell with a placeholder Placeholderfor one of your cells cells , which corresponds to the type of data you received.

We perform all of these actions by creating a placeholderContext placeholder context that controls the placeholder Placeholderand which you get from the coordinator coordinator by asking to “reset” Dropthe item element on the placeholder Placeholder.

I will use the initializer for the placeholderContext placeholder context, which “throws” dragItem to a UICollectionViewDropPlaceholder :



The object I'm going to “drop” Dropis item.dragItem , where item is the for element of the loop, since we can “throw” Dropmany coordinator.items objects . We “throw” them one by one. So item.dragItem is what we “drag” Dragand “throw” Drop. The next argument to this function is the placeholder, and I will create it using the initializer UICollectionViewDropPlaceholder :



In order to do this, I need to know WHERE I am going to insert the placeholderPlaceholder, that is, insertionIndexPath , as well as the reuseIdentifier cell identifier .
The insertionIndexPath argument is obviously equal to destinationIndexPath , this is the IndexPath to accommodate the “drag and drop” object, it is calculated at the very beginning of the performDropWith method .

Now look at the reuse cell identifier of the reuseIdentifier . You need to decide what type of cell cell is your a placeholder Placeholder. The coordinator coordinator does not have a “pre -assembled ” cell for the locator.Placeholder. It is YOU who must decide on this cell . Therefore, a reuse cell identifier reuseIdentifiercell is requested with yours storyboardso that it can be used as a PROTOTYPE.

I will call it “DropPlaceholderCell”, but in principle, I could call it whatever you like.
This is just a String string , which I am going to use on mine storyboardto create this thing.
Go back to ours storyboardand create a cell cell for the placeholder Placeholder. To do this, we just need to select a collection Collection Viewand inspect it. In the very first field, ItemsI change 1to2. This immediately creates a second cell for us, which is an exact copy of the first one.



Select our new cell ImageCell, set the identifier “ DropPlaceholderCell”, remove all UIelements from there , including Image View, since this PROTOTYPE is used when the image has not yet arrived. Add a new activity indicator from the Objects Palette Activity Indicator; it will rotate, making it clear to users that I am expecting some “discarded” data. Also change the background color Backgroundto understand that when "Reset" from the outside image works exactly this cell cell as prototypes:



addition of a new cell type must not be ImageCollectionVewCellbecause there will be no images in it. I will make this cell a regular cell of the UIollectionCiewCell TYPE , since we do not need any Outletscontrol:



Let's configure the activity indicator Activity Indicatorso that it starts animating from the very beginning, and I wouldn’t have to write anything in the code to run it. To do this, click on the options Animating:



And that's all. So, we made all the settings for this cell DropPlaceholderCell, go back to our code. Now we have an excellent replacement locator Placeholder, ready to go .

All that is left for us to do is receive the data, and when the data is received, we simply say about this context placeholderontext and it swaps the placeholderPlaceholderand our “native” data cell, and we will make changes in the Model.

I'm going to “load” the ONE object that my item will be using the loadObject method (ofClass: UIImage.self) (singular). I use the item.dragItem.itemProvider code with the itemProvider provider , which will provide me with item element ASYNCHRONOUS data . It is clear that if iitemProvider was connected , then the object of “reset” iitem we get outside of this application. The following is the loadObject method (oflass: UIImage.self) (in the singular): This particular closure is NOT executed on



main queue. And, unfortunately, we had to switch to main queueusing DispatchQueue.main.async {} in order to “catch” the aspect ratio of the image in the local variable aspectRatio .

We really entered two local variables imageURL and aspectRatio ...



... and will “catch” them when loading the image and image url URL :



If both local variables imageURL and aspectRatio are not nil , we will ask for the placeholderConttext placeholder context using the commitInsertion methodgive us the opportunity to change our Model imageGallery :



In this expression, we have insertionIndexPath - this is the indexPath to insert, and we change our Model imageGallery . This is all we need to do, and this method AUTOMATICALLY replaces the placeholder Placeholderwith a cell by calling the normal cellForItemAt method .

Note that insertionIndexPath can be very different from destinationIndexPath . Why?Because data sampling may take 10 seconds, of course, unlikely, but it may take 10 seconds. During this time in the collection Collection Viewcan very much happen. New cells can be added cells , everything happens fairly quickly.

ALWAYS use insertionIndexPath , and ONLY insertionIndexPath , to update your Model.

How do we update our model?

We insert into the array imageGallery.images structure imagemodel , composed of the aspect ratio aspectRatio and image URL imageURL , who gave us back the corresponding by provider .

This updates our ImageGallery model , and the commitInsertion method does the rest for us. No more do you need to do anything extra, no inserts, delete rows, none of this. And, of course, since we are in a closure, we need to add self. .



If we are for some reason not able to get the aspect ratio aspectRatio and URLimage imageURL from the corresponding by provider , an error might have been received error instead by provider , we have to let them know the context placeholderContext , you need to destroy this a placeholder Placeholder, because we are all the same, we can not to get other data:



It is necessary to keep in mind one particularity URLsthat comes from places like Google, in fact, they need minor transformations to get a “clean”URLfor the image. How this problem is solved can be seen in this demo application in a file Utilities.swifton Github .
Therefore, when retrieving an URLimage, we use the imageURL property from the URL class :



And this is all that needs to be done to take something from the outside on the collection Collection View.

Let's see it in action. We start simultaneously in a multitasking mode our demo application ImageGalleryand Safariwith a search engine Google. In Googlewe are looking for images on the theme "Dawn" (sunrise). In Safarialready builtDrag & Dropmechanism, so we can select one of these images, hold it for a long time, move it a little and drag it to our Gallery of Images.



The presence of a green plus sign "+" indicates that our application is ready to accept a third-party image and copy it into its collection at the location specified by the user. After we “reset” it, it takes some time to load the image, and at that time it works Placeholder:



After the download is complete, the “discarded” image is placed in the right place, and Placeholderdisappears:



We can continue “resetting” the images and placing them in our collections of even more images:



After the "reset" work Placeholder:



As a result, our Image Gallery is filled with new images:



Now that it's clear that we are able to receive images OUTSIDE, we no longer need test images and we remove them:



Our viewDidLoad becomes very simple: in it we make ours Controller Dragand Dropdelegate and add the pinch gesture recognizer , which controls the number of images on the line:



Of course , we can add a cache for imageCache images :



We will fill imageCache when “reset” Dropin the performDrop method ...



and when sampling from the “network” in the custom ImageCollectionViewCell class :



And we will use the imageCache cache when playing the cellcell of our Image Gallery in user class ImageCollectionViewCell :



Now we start with an empty collection ...



... then we "throw" a new image on our collection ...



... image loading takes place and itPlaceholderworks ...



... and the image appears in the right place:



We continue to fill our collection OUTSIDE: It



comes loading images andPlaceholdersworking ...



And images appear in the right place:



So, we can do a lot with our Image Gallery: fill it with EXTERNAL, reorganize the elements INSIDE, share images with other applications niyami.
It remains for us to teach her to get rid of unnecessary images by "resetting" them.Dropin the trash can presented on the navigation bar on the right. As described in the “Image Gallery” demo application features section, the trash can is represented by the GabageView class , which inherits from UIView, and we have to teach it to take images from our collection ollection View.

Reset DropGallery images into the trash can.


Right off the bat - in the quarry. I will add a “interaction” interaction to the GabageView and this will be a UIDropInteraction , as I try to get a “reset” of some thing. All we have to provide this UIDropInteraction is the delegate delegate , and I’m going to assign myself, self , to this delegate delegate : Naturally, our GabageView class must confirm that we are implementing the UIDropInteractionDelegate protocol : All we need to do to make it work , This is to implement the methods already known to us canHandle ,Drop







DropsessionDidUpdate and performDrop .



However, unlike similar methods for the collectionCollection View, we do not have any additional information in the form of the drop- point indexPath .

Let's implement these methods.
Inside the canHandle method, only “drag and drop” will be processedDrag, which are UIImage images. Therefore, I will return true only if session.canLoadObjects (ofClass: UIImage.self) :



In the canHandle method , in essence, you simply report that if the "drag and drop" object is not a UIImage imagethen it does not make sense to continue dropping the Drop and invoking subsequent methods.
If the "drag and drop" object is a UIImage image , then we will execute the sessionDidUpdate method . All we need to do in this method is to return our reset request to UIDropProposalDrop . And I am ready to accept only the “drag and drop” LOCALLY object of the UIImage image TYPE , which can be “dumped” Dropanywhere inside my GarbageView . My GarbageView will not interact with images dumped EXIT. Therefore, I analyze using the variable session.localDragSession, whether there is a local “drop” Drop, and return the “reset” clause in the form of the UIDropProposal constructor with the operation argument set to .copy , because ALWAYS LOCAL drag and drop Dragin my application will come from the collection Collection View. If there is a “drag Dragand drop” and “reset” DropOUTSIDE, then I return the “reset” clause in the form of the UIDropProposal constructor with the operation argument set to .fobbiden , that is, “forbidden” and we will get the sign of the “reset” prohibition instead of the green plus sign .



Copying UIImage image, we will simulate reducing its scale to almost 0, and when the “reset” happens, we will remove this image from the collection Collection View.
In order to create the illusion of “discarding and disappearing” images in the “trash can” for the user, we use the new previewForDropping method for us , which allows redirecting the “dumping” Dropto another place and at the same time transforming the “discarded” object during animation:



With this method, using the initializer UIDragPreviewTarget, we will get a new preView for the target object to be dropped and redirect it using the retargetedPreview method .to a new place, to the “trash can”, with its scale decreasing to almost zero:



If the user lifted his finger up, then a “reset” occurs Drop, and I (like GarbageView ) receive the message performDrop . In the message performDrop we perform the actual "reset" Drop. Honestly, the image itself, discarded on the GarbageView , no longer interests us, since we will make it almost invisible, most likely the fact that the “reset” is complete Dropwill signal that we remove this image from the collection Collection View. In order to do this, we need to know the collection itself and collection and indexPathdumped image in it. Where can we get them from?

Because the process Drag & Droptakes place in a single application, it is available to us all the local: local Dragsession localDragSession our Dropsession the session , the local context localContext , which is our collection of sollectionView and local object localObject , which we can do by itself is reset image image from "Gallery" or indexPath . Because of this we can get in the method performDrop class GarbageView collection collection , and using itdataSource how ImageGalleryCollectionViewController and Model imageGallery ourController, we can get an array of images of images TYPE [ImageModel]:



With the help of the localDragsession localDragSession ourDropsession session we were able to get all the "drag" on GarbageView Drag elements of items , and there may be a lot, as we know, and all of them are images of our collectionView collection . Creating theDragelements ofour collection's dragItemsCollection View , we provided for each “overtightened”Dragelement.dragItem local object localObject , who is the image of image , but it is we do not come in handy during internal reorganization collection CollectionView , but the "reset" Image Galleries "trash can" we desperately need in the local facility localObject "drag" object dragItem , after all this time we do not have a coordinator coordinator who so generously shares information about what is happening in the collectionView collection . Therefore, we want the local object localObject to be the indexPath in the image array images of our ModelimageGallery . Make the necessary changes in the method dragItems (at indexPath: IndexPath) class ImageGalleryCollectionViewController :



Now we can take every "pretaskivaemogo" element item it localObject , which is the index indexPath in the image array images of our models imagegallery , and send it to the array indexes indexes and array indexPahes delete images:



Knowing the index array indexes and array indexPahes delete images in the method performBatchUpdatescollection collection we remove all deleted images from Models images and from the collection of collection :



Run the application, fill the gallery with new images:



Select a pair of images that we want to remove from our gallery ...



... "throw" them on the icon with the "garbage bin" ...



They reduced almost to 0 ...



... and disappear from the collection Collection View, hiding in the "garbage can":



Saving images between runs.



To save the Gallery of images between launches, we will use UserDefaults , first converting our Model into a JSONformat. To do this, we add var defailts ... ... to our Controllervariable , and the Codable protocol in the ImageGallery and ImageModel structures : String strings , Array , URL and Double arrays already implement the Codable protocol , so we don’t have to do anything else to make the encoding work and decoding for the ImageGallery model in the format.







JSON
How do we get a JSONversion of ImageGallery ?
To do this, we create a calculated variable var json , which returns the result of an attempt to convert itself, self , using JSONEncoder.encode () to the JSONformat:



And that is all. Either Data data will be returned as a result of the self conversion to the format JSON, or nil if this conversion cannot be performed, although the latter never happens because this TYPE is 100% Encodable . The Optional json variable is used simply for symmetry reasons.
Now we have a way to convert the ImageGallery model to the Data format JSON. At the same time json variable has TYPE Data? which can be memorized in UserDefaults .
Now imagine that somehow we managed to get jsonJSON data , and I would like to recreate our Model, an instance of the ImageGallery structure, from them . To do this, it is very easy to write an INITIALIZER for ImageGallery , whose input argument is json data . This initializer will be a “falling” initializer (JSONfailable). If it fails to initialize, then it “falls” and returns nil :



I just get the newValue value using the JSONDecoder decoder , trying to decode the json data that is sent to my initializer, and then assign it to self .
If I managed to do this, then I get a new instance of ImageGallery , but if my attempt fails, then I return nil , because my initialization failed.
I must say that here we have a lot more reasons to “fail” ( fail), because it is quite possible that the jsonJSON datamay be corrupted or empty, all of which can lead to the “fall” ( fail) of the initializer.

Now we can implement the READ JSONdata and recovery model imagegallery the method viewWillAppear our Controller...



... as well as an entry in the observer didSet {} properties imagegallery :



Let's run the application and fill our gallery of images:



If we close the application and open it again, we can see our previous gallery images, which is stored in UserDefaults .

Conclusion


In this article, using the example of a very simple demo of the Gallery of images, it is shown how easy it is to embed technology Drag & Dropin an iOSapplication. This allowed us to fully edit the Gallery of Images, "throwing" there new images from other applications, moving existing ones and removing unnecessary ones. And also distribute images accumulated in the Gallery to other applications.

Of course, we would like to create many such thematic pictorial collections of images and save them directly on the iPad or on iCloud Drive. This can be done if each such Gallery is interpreted as a permanently stored UIDocument document .. This interpretation will allow us to rise to the next level of abstraction and create an application that works with documents. In such an application, your documents will be shown by the DocumentBrowserViewController component , which is very similar to the application Files. It allows you to create UIDocument documents of the “Gallery of Images” type both on your iPadand on iCloud Drive, as well as select the necessary document for viewing and editing.
But this is the subject of the next article.

PS The code of the demo application before the implementation of the mechanism Drag & Dropand after is on Github .


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


All Articles