Detect user dragging items out of UICollectionView? - ios

I've got a UICollectionView, and I'd like to be able to touch-and-drag items up and out of the View, and thus delete them. (Very much along the same lines as how the Dock works on OS X: drag something off and let go, and it is removed).
I've done some research, but almost everything I find is looking for CollectionViews that are drag-and-drop to reorder. I don't need to reorder (I'm happy to just remove the item at the given index from the source array and then reload), I just need to detect when an item is moved outside of the View and released.
So I suppose my questions are these:
1) Is that possible with the built-in CollectionView, some kind of itemWasDraggedOutsideViewFromIndex: method or something?
2) If not, is it something that can be done with a subclass (and specifically is it possible for a CollectionView beginner)?
3) Are there any code samples or tutorials you can recommend that do this?

Here is a helper class that I've been working on that does just that: implementation: https://github.com/Ice3SteveFortune/i3-dragndrop, hope it helps. There's examples on how to use it in the TestApp.
UPDATE
About a year on, this is now a full-on drag-and-drop framework. Hope this proves useful: https://github.com/ice3-software/between-kit

There is no built-in method like you're suggesting. What you're wanting to be can be done but you'll have to handle it with a gesture recognizer and appropriate code to handle the drag/drop operation.
I tried using a subclass to do this and finally went back to putting it in my view controller. In my case, though, I was dragging stuff in/out of the collection view as well as two other views on the screen.
I don't know if you have the book, but the most helpful thing I found was Erica Sadun's Core iOS6 Develper's Cookbook, which has excellent code on drag/drop within Collection Views. I don't think it specifically addresses dragging outside of the CV, but for me the solution was to put the gesture recognizer on the common superview and always use its coordinates rather than the subview's coordinates.
One problem I hit was I wanted to be able to select cells with a tap as well as drag, and there is no way (despite Apple's docs to the contrary) to require the single tap gesture to fail on the collection view. As a result, I ended up having to use the long press gesture to perform the entire operation, and there is no translationInView for long press (there is locationInView) so that required some additional work:
iOS - Gesture Recognizer translationInView
Another thing that will make it harder or easier is the number of possible drop targets you have. I had many, in many different types of views (straight UIView, collectionview, and scrollViews). I found it necessary to maintain a list of "drop targets" and to test for intersections with targets as the dragged object was moved. Somehow, you have to be able to determine whether the view you're intersecting is a place where a drop can occur.
If you are addressing the specific situation of dragging something out of a view to delete it (like dragging to a trash can view) and that's it, this should not be complicated. You have to remember that when you do a transform your frame becomes meaningless, but the center is still good; so you end up using the center for everything that you would normally use the frame for.
Here is the closest thing I found online that was helpful; I didn't end up using this class though as I thought it would be too complicated to implement in my app.
http://www.ancientprogramming.com/2012/04/05/drag-and-drop-between-multiple-uiviews-in-ios/
Hope this has been some help.

Yes there is.
1 - Conform your view to UIDropInteractionDelegate.
2 - Then add this line to your viewload or init:
For viewcontroller add to ViewDidload:
self.view.addInteraction(UIDropInteraction(delegate: self))
Or, for UIViews add to init:
self.addInteraction(UIDropInteraction(delegate: self))
3 - Then get the location for item being dragged here and have fun with it:
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
print(session.location(in: self))
return UIDropProposal(operation: .move)
}

Related

Swift - Performance of `UIGestureRecognizer` when having many of them

I've been thinking about this for quite some time now and I haven't found a suiting answer to this.
How performant are UIGestureRecognizer in swift/iOS development?
Let me explain by giving you a theoretical example:
You have an app on the iPad Pro (big screen, much space) and there you have maybe dozens of different views and buttons and so on. For whatever reason you need every one of these views and buttons to be moveable/clickable/resizable/...
What's better?
Adding one (or multiple) UIGestureRecognizer(s) to each view (which results in many active gesture recognizers and many small, specific handling methods [maybe grouped for each type of view])
Adding one single recognizer to the superview (which results in one active gesture recognizer and a big handling method that needs to cycle through the subviews and determine which one has been tapped)
I guess the first one is the most simple one but is it slower than the second one? I'm not sure about that. My stomach tells me that having that many UIGestureRecognizers can't be a good solution.
But either way, the system has to cycle through everything (in the worst case), be it many recognizers or many subviews. I'm curious about this.
Thank you
Let look at your question in terms of gesture recognition flow -> to pass event to the right gesture recognize the system goes by the views tree to find last one specific to this gesture, that will return true in one specific method of UIView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
Adding one (or multiple) UIGestureRecognizer(s) to each view
This way I recommended to use. In this case the system will do most of the work for you and prevent you from mistakes that very difficult to debug later - trust me. This is for UI. Especially if you have multiple different gestures on different parts of a screen. In my particular case I have a huge video player UI that has around 20 gesture recognizers on one screen and feels pretty good - no lags or frame drop in UI. this way is simple and self describing. I recommend implement it using storyboard or xib. You can refer to Interface Builderand later any time to recall in a moment what recognizer should you update to change the behaviour of UI. The speed of this approach guaranteed by a system.
Adding one single recognizer to the superview
This way could be used with only one simple gesture for a multiple views (more > 20). this could happened if you implementing some game where user pick up and paste a bricks of different shapes for example. It is not suitable for common UI tasks. The speed depends on your implementation, and based on question itself I am not recommend to do it. This approach is design specific not speed relevant.

Can two UICollectionViews respond to a single gesture?

I have two fullscreen child UICollectionViews. One is a transparent overlay on the other. I'd like them both to respond when I drag around the screen - both of them when it's a horizontal drag and only one of them when it's a vertical drag, a little like some media centre home screens. Is this possible without reimplementing the private UICollectionView gesture recognisers, and if so how?
If not then any pointers to example reimplementations would be appreciated.
Some things I know, or have tried:
I have a pan gesture recogniser on the View Controller with a Delayed Begin that can detect the vertical or horizontal movement before events are sent through to the views.
I know that simply forwarding events from my parent view's touchesBegan: etc. won't work because the touches' view property is set to my parent view, and UITouches can't be copied (naively at least) since they don't implement the NSCopying protocol. Perhaps I can synthesise suitable UITouch events and forward them?
I know I can send scrollToItemAtIndexPath:atScrollPosition:animated: messages manually but I'd prefer to have the natural drag, swipe and snap paging behaviour for the Collections.
Alternatively, is it possible to modify the private gesture recognisers' delegates and implement gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: - without explicitly accessing private APIs - to allow both collections to see the touches? Is the responder chain smart enough to call this with gesture recognisers from two sibling views?
Another approach might be to manually control the overlay, and not manage it as a Collection View, but Collection Views seem like a more natural fit, and in theory provide the interactivity I'd like out of the box. The box, at the moment, seems to need a crowbar to get in!
This question seems similar (if less explicit), and has no answers. The other questions I've looked at all seem to be about adding pinch, or having subviews of collections also respond to gestures; not quite my situation.
I'm scratching my head a little, so thanks for any pointers.
The short answer is you can't, easily, anyway.
The approach that worked for me is a lot simpler, and cleaner: embed one collection view within another. The containing one is limited to horizontal scrolling, and the overlay one to vertical, both with paging turned on. Both share the same controller as their delegate and datasource, and - since a collection view is a subclass of scroll view - this also keeps track of which container and overlay page we're on in the scrollViewDidEndDecelerating: method:
-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if ([scrollView isEqual:containerCollection]) {
containerNumber = scrollView.contentOffset.x / scrollView.frame.size.width;
}
else {
overlayNumber = scrollView.contentOffset.y / scrollView.frame.size.height;
}
}
The only real bit of trickery was in my cellForItemAtIndexPath: method where, when I instantiate the container cell, I need to register .xibs for reuse (each overlay is different) and use the remembered overlay page and issue both scrollToItemAtIndexPath: and reloadItemsAtIndexPaths: to the embedded overlay collection to get it to appear correctly.
I've managed to keep both cells as separate .xibs as well, with associated convenience classes for any extra data they need (and in the case of the container collection the overlay collection IBOutlet).
And not a gesture recogniser in sight.

Dragging an uiview like facebooks menu slide

I know this has been probably asked before but I've seen many approaches and i don't know which is best for me, so plz don't send me a link to another post unless it addresses my problem directly.
I have a controller which has a uiview on the top (like a header) (this header is bigger than it seems because is partially hidden on top). on that view i have a uibutton which now with a touch up inside shows the entire header view and taping again returns it to its starting position (changing frame with animation). I want to also be able to drag the view but only changing position on the y axis(dragging up and down)... i was thinking of adding the dragInside/Outside event to the button but this doesn't give me the position of the finger... and also want to know when the user releases the drag so the view ends animation to any of its two possible states (showing or partially hidden). Is this a "touches began" , "touches moved" , "touches ended" thing? if it is please provide a code example. I also want to do this with another view but this is on the left side... same thing but this one moves on the X axis... any help is appreciated. or maybe it can be made with drag event if i only can save a CGpoint of last touch, maybe that's better, any other suggestions
Look at using a UIPanGestureRecognizer to detect the touch movements. Use the translationInView: of the gesture to set the view y position. The translation is the total movement since the start of the gesture so you don't need to remember and accumulate the offset position yourself.
The main thing to worry about while implementing this is bounding the y position of the view so that no matter how far the user drags the view won't go too high or low on the screen.
Use a UIPanGestureRecognizer, that's a class dedicated to handling such drag/pan gestures.
Everything is described here in Apple's documentation, including examples, so you should find your answer here.
There is also some sample code in Apple Developer Library that shows you how to use Gesture Recognizers if needed.

How to make SWTableViewCell swipe more sticky

I've implemented the SWTableViewCell code in order to allow side swipes to reveal more UI, such as buttons.
Which is working fine, except that the UIScrollview it subclasses is really just too touchy, flicking back and forth like a manic game of ping pong.
I went to make changes to the class, but realised UIScrollView didn't seem to give me the ability to say change the way the scrolling animations work in the way I wanted.
Leaving me thinking that I either need to create my own version of the a Swipe cell, with a pan gesture and a overlay view, instead of a scrollview or find someone who has already solved this problem.
Please check it out sample: https://github.com/runmad/RMSwipeTableViewCell
May be it will helpful to you, Sir
:)
Not sure if this will give you the effect you desire, but you can make it less "jumpy" by altering the decelerationRate. Try:
someScrollView.decelerationRate = UIScrollViewDecelerationRateFast;
If that's the right idea, you can jump-to-definition in Xcode, and checkout the float value, then try your own.

Gestures that steal touches like iOS multitasking swipe

I know what I want to do, but I'm stumped as to how to do it: I want to implement something like the iOS multitasking gestures. That is, I want to "steal" touches from any view inside my view hierarchy if the number of touches is greater than, say, two. Of course, the gestures are not meant to control multitasking, it's just the transparent touch-stealing I'm after.
Since this is a fairly complex app (which makes extensive use of viewController containment), I want this to be transparent to the views that it happens to (i. e. I want to be able to display arbitrary views and hierarchies, including UIScrollViews, MKMapViews, UIWebViews etc. without having to change their implementation to play nice with my gestures).
Just adding a gestureRecognizer to the common superview doesn't work, as subviews that are interaction enabled eat all the touches that fall on them.
Adding a visually transparent UI-enabled view as a sibling (but in front) of the main view hierarchy also doesn't work, since now this view eats all the touches. I've experimented with reimplementing touchesBegan: etc. in the touchView, but forwarding the touches to nextResponder doesn't work, because that'll be the common superview, in effect funnelling the touches right around the views that are supposed to be receiving them when the touchView gives them up.
I am sure I'm not the only one looking for a solution for this, and I'm sure there are smarter people than me that have this already figured out. I even suspect it might not actually be very hard, and just maybe my brain won't see the forest for the trees today. I'm thankful for any helpful answers anyway :)
I would suggest you to try using method swizzling, reimplementing the touchesbegan on UIView. I think that the best way is to store in a static shared variable the number of touches (so that each view can increment/decrement this value). It's just a very simple idea, take it with a grain of salt.
Hope this helps.
Ciao! :)
A possible, but potentially dangerous (if you aren't careful) approach is to subclass your application UIWindow and redefine the sendEvent: method.
As this method is called for each touch event received by the app, you can inspect it and then decide to call [super sendEvent:] (if the touch is not filtered), or don't call it (if the touch is filtered) or just defer its call if you are still recognizing the touch.
Another possibility is to play with the hitTest:withEvent: method but this would require your stealing view to be placed properly in the subview, and I think it doesn't fit well when you have many view controllers. I believe the previous solution is more general purpose.
Actually, adding a gesture recognizer on the common superview is the right way to do this. But it sound like you may need to set either delaysTouchesBegan or cancelsTouchesInView (or both) to ensure that the gesture recognizer handles everything before letting it through to the child views.

Resources