Drag & Drop in your iOS apps

  • Tutorial


The mechanism Drag & Dropworking in iOS 11and iOS 12is 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 iOSdue to the fact that while dragging something in iOS, multitouchit allows you to freely interact with the rest of the system and collect data for resetting from different applications.

iOSmakes 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. This freedom of action is possible on iPad, on the iPhonearea of ​​coverage Drag & Dropin iOSlimited by one application.

The most popular apps ( Safary, Chrome, IbisPaint X, Mail, Photos, Files, etc.) are already built-in mechanism Drag & Drop. In addition to thisApplemade it very simple and intuitive APIfor developers to embed the mechanism Drag & Dropinto your application. The mechanism Drag & Drop, just like gestures, works on UIView and uses the concept of “interactions” Interactions , which are a bit like gestures, so you can think of the mechanism Drag & Dropsimply as 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 it has been API Drag & Dropimproved and raised to a higher level of abstraction for them in that the collection Collection Viewitself helps you with indexPathcollection item that you want to drag Drag. She knows where your finger is and interprets it as the indexPath of a collection item that you are dragging Dragat the moment or as the indexPath of a collection item where you drop Dropsomething. So the collection Collection Viewprovides you with indexPath , but otherwise it’s absolutely the same API Drag & Dropas for a regular UIView .

Process Drag & Dropfor iOShas 4 distinct 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 move ( 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 session " Drag and Drop:



Drop


Drop occurs when the user lifts a finger. At this point, two things can happen: either the Dragobject will be destroyed, or the object will be “dumped” Dropat the destination.



Data Transfer


If the process of "dragging" Drag has not been revoked and held a "reset" Drop , what occurs Data Transfer (data transmission) at which the "reset point" requests data from the "source", and there is asynchronous data transfer.

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 a mechanism Drag & Dropin your iOSapplication.
We will provide the collection with the Collection View ability to fill ourselves with images from outside, as well as reorganize the elements INSIDE yourself using the mechanismDrag & Drop. In addition, this mechanism will be used to reset unnecessary elements of the collection Collection Viewin the "trash can", which is a normal UIView and is represented by a button on the navigation bar. We will also be able to share, using the mechanism, Drag & Dropimages collected in our Gallery from other applications, for example, with “Notes” ( Notesor Notability) or with mail Mailor with a photo library ( Photo).

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

Features of the “Image Gallery” demo application


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



central part of the application is unconditionally Image Gallery Collection View Controllersupported 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 , in which each image is described by structure struct ImageModel containing the URLurl of the image location (we are not going to store the image itself) and its aspect ratioaspectRatio :



Our ImageGalleryCollectionViewController implements the DataSource Protocol:



Custom cell collection cell contains an image imageView: UIImageView! and spinner activity indicator : UIActivityIndicatorView! and supported by the usersubclass ImageCollectionViewCell class UICollectionViewCell :



Public APIClass ImageCollectionViewCell - thisURLimage imageURL . As soon as we install it, ours isUIupdated, 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 a URLglobal global queue (qos: .userInitiated) to get data for a given queue with the quality of service argument  qos , which is set to .userInitiated , 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“ circular reference to memory ”(memory cycle)? ”We do not have an explicit“ cyclic memory reference ”( memory cycle) here, 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 Collection Vieware reusable due 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 ).

As soon as the download is complete and the image is received, an update occurs.UI this cell collection. But we do not wait for the image to load, we continue to scroll through the collection and the collection cell that we notice goes off the screen without updating its own UI. However, a new image should appear from below, and the same cell that has left the screen will be reused, but for a different image, which may load and update quickly 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 limits of the GCD mechanism used by us we cannot cancel the loading of an image of a cell that has left the screen, but when our data from imageData comes from the network , check the URLurl that caused the download of this data and compare it with what the user wants in this cell at the moment, there is imageURL . If they do not match, we will not update the UIcell and wait for the image data we need:



This seemingly absurd code line url == self.imageURL makes everything work correctly 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 Collection Viewcan confuse the user a little:



We would not like the image with an 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 have to communicate this wish to ours Controllerso that he corrects the aspectRatio aspect ratio in his ImageGallery Model .for the corresponding indexPath . This is an interesting problem, there are many ways to solve it, and we will choose the easiest of them - using the Optional closure ( closure) var changeAspectRatio: (() -> Void)? . It can be nil and need not be set if this is not necessary:



When I call a closure, change AspectRatio? () In case of an erroneous data sample, 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 exactly what we do in our cellForItemAtController method :



Details can be found here .

The sizeForItemAt delegate UICollectionViewDelegateFlowLayout method is used to display images with the correct aspectRatio : In addition to the image collection , we placed a button with a custom GarbageView image containing the “trash can” as a subview on our navigation bar : In this figure, the background colors for the GarbageView itself  and the buttons are specially changed Uibutton



Collection ViewUIBar Button



with the image of a "trash can" (actually there is a transparent background) so that you can see that a user who "dumps" Gallery images into a "trash can" has much more room for maneuver when dropping a  Drop than simply 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 with the “garbage bin” image taken from the standard button  as a   subview : I set the transparent background for the GarbageView :Bar ButtonTrash







"Garbage can" size and position of the place will be determined by the method layoutSubviews () class UIView , depending on the boundaries bounds of UIView :



This is the initial version of the sample application "Image Gallery" is located in the Githubfolder ImageGallery_beginning. 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 EXTERNAL:



The plan for introducing the mechanism Drag & Dropinto our application is as follows:

  1. First, we will give our collection of images the Collection Viewability to “drag” UIImageDrag images from it both externally and locally,
  2. then we will teach our collection of images Collection Viewto take UIImageDrag images from outside or locally ,
  3. We also teach our GarbageView  button "garbage can" take to "pull" from a local collection of Collection Viewimages UIImage and remove them from the collectionCollection View


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 a mechanism in place Drag & Drop. She is on the Githubin folder ImageGallery_finished.

The efficiency of the mechanism Drag & Dropin your collection is Collection Viewensured by two new delegates.
The methods of the first delegate, dragDelegate , are configured to initialize and customize drag and drop Drags.
The methods of the <u second delegate, dropDelegate , complete the “drag Dragsand drop” and basically provide the data transfer ( Data transfer) and custom animations when the “reset”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 only need “overtightening” Dragor only “dumping” Drop, but you can use both protocols at once and perform both “dragging” Dragand “reset” simultaneously Drop, which opens up additional functionality Drag & Dropfor the reordering mechanism items in your collection Collection View.

Dragging and dropping Dragitems from the collectionCollection View


Implementing a Dragprotocol is very simple, and the first thing you should always do is set yourself, self , as a dragDelegate delegate :



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



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



The only mandatory method of the UICollectionViewDragDelegate protocolis the itemsForBeginning method , which will tell the Dragsystem what we are dragging. Method itemsForBeginning called when the user begins to "drag and drop" ( Dragging) cell collection cell .

Notice that the collection Collection Viewadded the 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 and drop Drag.

If an array is returned[UIDragItems] of the overtightened ” elements, the “overtightening” isDraginitialized, but if the empty array [] is returned, the “overloading” isDragignored.

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 operation Drag & Dropis completely asynchronous. When you start dragging Drag, this is really a very lightweight object ( lift preview), you drag it everywhere, and nothing happens during this dragging. But as soon as you “throw” Dropyour object somewhere, it is itemProvider, really should provide your “dragged” and “dropped” 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 iOSand that are itemPoviders , such as for example NSString, 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 Webpage, select URLand “throw” it wherever you want. This may be a link to an article orURLfor an image like this in our demo. This color classes UIColor , map element MKMapItem , contact CNContact from the address book, a lot of things you can choose and "drag and drop". All of them are itemProviders .

We're going to drag a UIImage image . It is in the collection cell Collection Viewwith the indexPath , which helps me to select the cell , get OutletimageView from it and get its image .

Let's express this idea in a couple of lines of code.
First I request my collection Collection Viewabout cellscell for the item elementcorresponding to this indexPath .



The cellForItem method (at: IndexPath) for the collectionCollection Viewonly works for visible (visible) cells, but, of course, it will work in our case, because I “drag”Dragthe collection 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 usersubclass. And if it works, then I get anOutlet imageView , from which I take its image . I just captured the 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 using the UIDragItem constructor , which takes the itemProvider argument :



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



this NSItemProvider constructor, 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 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 will come in handy later:



By starting dragging Drag, you can add more items to this drag-and-drop by simply performing a tap gesture 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 indexPath of what the user “tapped” on during the drag and drop process.Drag, and it is enough for me to receive the image 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 application Photos, since you see a green plus sign "+" in the upper left corner of the "drag and drop" image. I can perform a tap gestureone more image of “Artik” from the collection Collection View...



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



Since the application Photoshas a mechanism already built in Drag & Drop, everything works fine and it's 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's 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 View OUTSIDE.
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  :



I have to determine if 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 methoditemsForVeginningDrag 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, further it makes no sense to talk of a "reset" 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 parameter will 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 engine Google, 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 is provided to us by the coordinator coordinator . The first and most important thing that the coordinator tells usThe coordinator is the destinationIndexPath , that is, the destination-point “reset” indexPathDrop , that is, where we are going to “reset.”



But destinationIndexPath can be equal to nil , since you can drag the “dumped” image to a part of the collectionCollection 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 elementin the 0th section of the section .



I could choose any other indexPath , but this oneI will use indexPath 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 TYPEand 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 self-made, self , and the source for the dragDragis the collection item with indexPathequal 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 destinationIndexPathDrag & 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 need it when "reset"Dropimages in 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 .

First, I get imageInfo information 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 . That's 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" process  Drag, because it resets the whole "world" of our gallery of images, which is very bad, I do not. Instead, I'm going to clean up and paste 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, the collection collectionView will be at some point in a NOT synchronized state with the Model.

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



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

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 "drop" item came from somewhere outside and we will have to enable data transfer using the itemProver drop " item.dragItem.itemProvider element :



If you “drag Drag and drop” something EXTERNAL and “throw” Drop, does this information become instantly available? 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 View substitute 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  Placeholder call its context.placeholderContext and 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 placeholderPlaceholder and which you get from the coordinator coordinator by asking to “reset”Dropthe item elementon the placeholderPlaceholder.

I will use the initializer for the context substitute. The placeholderContext that “throws” the dragItem to the  UICollectionViewDropPlaceholder :



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



In order to do this, I need to know WHERE I am going to insert a placeholder Placeholder, that is, insertionIndexPath , as well as the identifier for the reused cell  reuseIdentifier .
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. Coordinator coordinator There is no “pre -assembled ” cell cell for the substitute 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 1to 2. 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 UIСollectionCiewCell 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  placeholderСontext 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 (ofСlass: 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 method give 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 collectionCollection Viewa lot can 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 Model imageGallery , and the commitInsertion methoddoes everything else 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 URL image 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 Get other data:



One thing to keep in mindURLsthat come from places like Googlethat, in reality they need little transformations to get a “clean” URL 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 in order to take OUTSIDE something inside the collection Collection View.

Let's see it in action. We start simultaneously in a multitasking mode our demo application ImageGalleryand  Safari  with a search engine Google. In  Google we are looking for images on the theme "Dawn" (sunrise). ATSafaria Drag & Dropmechanism is already built in, 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  ControllerDragand 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 ourselves, 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 prohibition “reset” .



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 it dataSource how  ImageGalleryCollectionViewController  and Model imageGallery ourController, we can get an array of images of images TYPE [ImageModel]:



With the help of the local Dragsession  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  performBatchUpdates collection 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 will be returned as the 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 let's imagine that somehow we managed to get jsonJSON data , and I would like to recreate our Model from them, an instance of the ImageGallery structure . 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 Image Gallery, “throwing” new images there 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 will allow you to create UIDocument documents of the type “Image Gallery” 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 .


 

Also popular now: