Handling multiple UICollectionView' interactiveMovements - Crash: UIDragSnappingFeedbackGenerator is already being interacted with - ios

(See Update 3 for more information. I fixed the original problem, now there's an exception I've never seen before)
so I've been trying to implement my own version of IOS11' Drag&Drop feature. I've implemented a custom gesture recognizer and my own drag and drop session manager, called DragAndDropSession.
The situation with my app is the following: I have a "fullscreen" vertically scrolling collectionView, that holds horizontally scrolling collectionViews in each cell (row). A bit like what netflix has, for instance.
While dragging an item, I want these horizontal collectionViews to dynamically make space for the dragged item (just like with IOS11' Drag&Drop). I do that by adding an invisible cell to the row's collectionView and then I use collectionView.beginInteractiveMovement(..) on that cell. This way, it looks like a gap is moving around when I constantly update it to the current touch position. (I haven't found a better way).
Each time the touch moves to a different row, I stop the first interactive movement, remove the empty cell, and add it to the new collectionView, where I again begin an interactive movement. The difficulty here is the managing aspect so that everything gets "cleaned up" and then set up again correctly.
As this is hard to explain, I created a demo project showcasing everything (including the crash I'm going to tell you about in a second):
https://github.com/d3mueller/DragAndDropTest
(I hope it's working. Let me know, if not)
A few things:
It's a work in progress. dropping an item isn't implemented, so don't try that :D. Things will happen, that should not happen. I only implemented dragging
I'm using IGListKit (https://github.com/Instagram/IGListKit) to manage my collectionViews. It's probably not relevant to my problem, though. (I tried to comment the important bits)
In my code, you'll often see ...SectionController. This is the "manager" of a cell in the collectionView (IGListKit). For instance, the rows in the vertically scrolling collectionView each have a section controller that contains the data for this row, and the collectionView for this row etc.
In the following, I'll try to explain my problem. Sadly, I can't really post actual code snippets here, because you need to know the context to understand what happens there. That's why I added the demo project.
Okay, now to my problem: It's crashing. Sometimes. It gives me this error message:
'NSInternalInconsistencyException', reason: 'attempt to begin reordering on collection view while reordering is already in progress'
I set a breakpoint to catch this exception, so I know that it crashes in DragAndDropSession.swift: Line 194, which is this:
rowCollectionView.beginInteractiveMovementForItem(at: rowIndexPath)
In this line, I start a new interactive movement for the collectionView row that the finger is currently hovering on. I just don't know why it gets to this line, when it already has begun an interactive movement. A few lines above (line 171) I cancel the interactive movement.
There is a specific case/situation I'm not covering/catching in my code. I just can't find it. I've spent hours on this.
How to reproduce this bug (Look at Update 2. I found a way):
(I only managed to reproduce it on my iPad, not in the simulator)
Long press any item, move it a bit and then use another finger (while still holding the dragged item) to quickly scroll up and down and left and right. You have to be really fast and chaotic. Then it sometimes crashes.
The cause:
First, it's likely to be the updateDrag() method in DragAndDropSession.swift.
The error says I'm trying to begin reordering while another reordering is already in progress. Thus, in some cases I begin it twice or I don't cancel the movement before beginning another. I just don't know why.
UPDATE:
Sometimes, a different error is being trown (at the same line, after doing the exact same thing):
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<_UIDragSnappingFeedbackGenerator: 0x1c0134dc0: prepared=1> is already being interacted with'
I have never seen this before. The only thing I found about this, is this bug report: https://openradar.appspot.com/42139082
Any ideas?
UPDATE 2:
I found a way to reproduce it 100% of the time (I updated the demo project to add more rows and remove the empty row):
Steps to reproduce (also see gif):
long press an item to begin dragging it.
While still dragging the item, use the second finger to scroll at least 2 rows down. While you do this, you musn't move the first finger. It has to be perfectly still. Also, it has to be one swipe gesture to scroll.
Before the scrolling stops, scroll up (again using only one swipe gesture) until you reach the row directly below the one you started out with. Stop there (using the second finger or wait until it stops by itself)
Move the first finger (that's dragging the item) which will create a gap in that row.
Now, whenever you move the first finger onto the row you started out with, it crashes.
UPDATE 3:
I've fixed the original problem with the attempt to begin reordering on collection view while reordering is already in progress. It took me quite some time, the problem was occurring when a row leaves the screen (that's why you needed to scroll down quite a bit before it crashes). When the row then came back, a possibly different cell would be dequeued, thus I couldn't cancel the interactive movement from the original one. I fixed this by saving the collectionView itself (contained by the row). This and a couple small fixes did the trick. However, now I'm getting the following error (as introduced in Update 1) all the time it crashes.
2018-07-27 18:33:06.866322+0200 DragAndDropTest[62655:8122083] * Assertion failure in -[_UIDragSnappingFeedbackGenerator userInteractionStarted], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3698.54.4/_UIDragFeedbackGenerator.m:175
2018-07-27 18:33:06.867191+0200 DragAndDropTest[62655:8122083] * Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '<_UIDragSnappingFeedbackGenerator: 0x1c012c9e0: prepared=1> is already being interacted with'
I haven't found a reliable way to reproduce this yet, but it's fairly easy to trigger. Just move the dragging item around a bit and scroll etc.
The big problem now is that I have absolutely no idea what this is. I've never seen this before, I don't know how to debug it. Has anyone ever seen this?
Again, I updated the demo project.
I'd be extremely grateful if someone could take a look at this. It's probably something trivial I'm not seeing. Let me know if you need any more information
Thank you!

I had the same assertion failure using iOS 11 Collection View Drag and Drop API in combination with interactive move (beginInteractiveMove etc).
Since this scenario is very specific and edge-casey, your mileage may vary.
I ventured into the rabbit hole and figured out the following:
_UIDragSnappingFeedbackGenerator is a private subclass of UIFeedbackGenerator which is responsible for haptic feedback
UIFeedbackGenerator Apple Documentation:
Releasing the Generator
If you no longer need a prepared generator, remove all references to the generator object and let the system deallocate it. This lets the Taptic Engine return to its idle state.
This could mean that for some reason the interactive move / reordering session is not completely finished although you may have called cancelInteractiveMovement(). The feedback generator remains in a prepared state (→ reason: '<_UIDragSnappingFeedbackGenerator: 0x1c012c9e0: prepared=1> from the assertion failure log) and will not let go of this state until it is deallocated by the system.
What fixed it for me was overriding cancelInteractiveMovement in my UICollectionView subclass and calling both super.cancelInteractiveMovement and super.endInteractiveMovement:
override func cancelInteractiveMovement() {
super.cancelInteractiveMovement()
super.endInteractiveMovement() // ← will not perform the standard "end" animation
// the moving cell was already reset by cancelInteractiveMovement
}
This seems to clean up the unfinished UIFeedbackGenerator and does not crash anymore (so far).
Related links
http://www.openradar.me/42139082 (closed rdar)
http://www.openradar.me/42154922 (duplicate of above, still open)

Although it's been quite a while since this was originally asked, there's still little information on the <_UIDragSnappingFeedbackGenerator: 0x1c012c9e0: prepared=1> is already being interacted with' error out there and we were experiencing it quite regularly in our app even after implementing the override cancelInteractiveMovement() call in the answer from #Daniel.
When I dropped breakpoints on the cancelInteractiveMovement it was very rarely called.
I've implemented the following on the drag delegate for the collection views and so far have not seen any of the UIDragSnappingFeedbackGenerator crashes yet.
public func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {
collectionView.endInteractiveMovement()
}
Still TBD if there will be downstream consequences for this.

Related

SwiftUI Drag and Drop error "NSInternalInconsistencyException"

I have a swiftUI app that implements the native drag and drop apis. Everything works well but I have discovered an edge case that I don't understand how to fix. Essentially the entire view of the app (the size of the device screen) is a drop target. When dragging an object to be dropped, if the user moves the item to the edge of the screen the app will crash throwing the error:
'NSInternalInconsistencyException', reason: UIDragPreviewTarget requires that the container view is in a window, but it is not
I assume this has to do with the drop target and trying to move the drag object out of the app's window, but I don't know what to do to handle this edge case.
Any help would be greatly appreciated
EDIT:
I commented out the .onDrop() modifier and the crash still persists. I can only guess that the generated UIDragPreviewTarget is tracking its location internally but when it is pulled out of the Frame of the app it throws the error
After digging around a bunch more and creating a new simplified project to demonstrate this wasn't a bug in the Drag and Drop implementation in SwiftUI, I think I found the issue.
In my case I have a view that holds the data I need sitting above the view I want to be able to drag that data into. While in the drag process, I wanted the top view to dismiss itself so as to no longer obstruct the lower view. I was flipping this switch in one of two ways.
when starting the drag process on the original piece of data, I started a timer that would give you 1.5 seconds to cancel the drop before it would hide the view.
when entering the lower view's drop target, it would close the top view
Removing both of these actions eliminated the crash.
My assumption at this point is that hiding the original source of the data is tripping up the dragItem's sense of "home" and when you try and drag it off screen it has no where to return back too.

SWTableViewCell - No animation, but delegate methods are being called

I have spent countless hours trying to get SWTableViewCell working, and I've run out of ideas. I'm trying to integrate it into a UITableViewController that contains a custom UITableViewCell (subclassed). For some reason, I can't get any of the animation working. I thought at first that MMDrawerController might have been causing the problem, but after completely removing it's usage, the swipe still doesn't produce animation. So that's not the culprit.
I've gone so far as to try a different cell swipe implementation (TLSwipeForOptionsCell), but I get the same results of no action. I've also tried MCSwipeTableViewCell, which does work in showing the swipe action, but unfortunately presents it's own problems since it doesn't support auto-layout.
For the SWTableViewCell, I can confirm by stepping through the code that
The class receives the gesture and steps through the logic of the code appropriately.
The delegate methods are getting fired appropriately, so the control should have done what it was supposed to do.
However, nothing happens in my table view cell. No animation, no glitch/flicker, no sign that anything has changed.
I've also followed the guidance for using table view editing, which did remove the default "delete" option (desired to remove that anyway), but it still doesn't work.
As you can see in the documentation on GitHub, integrating this should be super simple, but it just doesn't work for me.
Target is iOS 7.1 SDK.
For SWTableViewCell (the swipe implementation I'd prefer to use), I've just noticed that the selection of the cell is lost almost immediately when starting to drag. If I touch and hold on the cell, it is selected. I move just slightly, and selection is lost. However, with MCSwipeTableViewCell the selection is not lost.
Any ideas?
At the expense of looking like an idiot, I'm going to log what the problem was just in case someone else makes the same mistake.
In addition to the symptoms above, I was also having a problem where touching on the cell so that the selection state was triggered would result in a highlight that covered all of my controls--the cell looked empty/blank. That was also resolved with the solution below.
In Interface Builder, I had set the backgroundView Outlet to contentView. Don't do that. Bad stuff happens.
Hope someone else ends up benefiting from this.

Animations during first keyboard appearance is sometimes not smooth

I have often noticed that UIView animations are often not smooth during the first becomeFirstResponder event when the keyboard appears for the first time. I am referring to animations that occur with the keyboard animation, such as manually scrolling the UIView to make a textField visible. The animation is always smooth after the first time it is executed.
Is there a technical reason why this would be the case? I was thinking that there might be some lazy loading or optimization that happens with UIView animations on the first run, then gets stored in cache for reuse. Are there lessons learned around this? If this is not clear to this audience, I can try to recreate the issue in a test project.
While this does not answer the question WHY this happens, it explains how to fix it.
Why are iOS animations slow the first time they are run?
Basically, you need to do animations on "DID" events rather than "WILL" or "SHOULD". The system performs it's animations during the "will/should" events, so apparently there is some colluding happening. This does not explain why the behavior is inconsistent between the first run and all other runs.
I thought, as may some of you, that I should put the animation in the "textFieldWillBeginEditing" because I wanted the animation to run concurrently with the keyboard animation. Luckily, putting the animation code in "DID" actually still ensures that the animation happens concurrently. Fantastic.
If anyone still has an explanation of the inconsistency between the first and latter runs, I'll still hold his question open and award you with an upvote and question answer. Thanks!

What's causing the delay in my segues?

One of my views freezes up for several seconds when I tap the back button.
In addition, when I tap on one of the items in this view, it shows a popup (custom, not a UIPopoverController). This popup appears quite fast, but when I "flip" the popup to see it's back side, the same long delay occurs.
I suspect the reason has something to do with the complexity of the view. As you can see in the screenshot below, it's a collection view, it has a background and some of the subviews are rotated (UIViewEdgeAntialiasing is on).
I used the Time Profiler in Instruments to figure out what's going on, but I'm stuck.
I don't see anything useful unless I deselect "Hide System Libraries":
If I look at the method names, I think they are related to auto layout. That suggests that it's trying to render something during the segue. But methods such as cellForItemAtIndexPath are not called.
There is also an iPhone version of this app where I don't experience this problem at all. It uses a tableview in stead of a collectionview. It also has a background and rotated pictures.
I took these measurements using the simulator; on my iPad Mini the situation is worse; it can take up to 20 seconds before the animation starts.
Update - Things I've tried thanks to your answers:
turn off UIViewEdgeAntialiasing : no effect on performance
I think this might be due to the UIViewEdgeAntialiasing flag. It seems that your main view (the one with lots of slightly rotated pictures) have lots of antialiased edges and hence is very taxing on the iPad's GPU. The fact that the drawing performance slows down when your popover is spinning (ie when the background is showing again) gives this some credence.
Try turning it off and see if the performance improves. How does it look like?
Rotation was the bad guy here. Each UICollectionViewCell has a UIView as a container view and within that is a UIImageView. I rotate it like this:
container.transform = CGAffineTransformMakeRotation(M_PI * someRandomFloat);
Remove that line and everything is snappy.
I use the same technique on the iPhone, but apparently this kind of rotation has less of a performance impact in UITableViewCell than in UICollectionViewCell.
I tried subclassing UICollectionViewFlowLayout to rotate cell itself in stead of one subview. Unfortunately that causes a similar performance issue.

Suppess UIView re-layout when child view changes

See UPDATE below:
I am confused about UIView layout as subviews are moving. I have a "Surface" UIView with several "Item" subviews on it. The user is allowed to click on the items and drag them around the surface.
What I have noticed though is that whenever a Surface's Item subview moves (is dragged by the user), the Surface is marked as needing to be relayed out.
I do not want the Surface to layout the Items after the user moved them. That is pretty much the whole point, I am allowing the user to position them as they see fit. However, there are times, initial creation is a prime example, where I do need the Surface to place the items.
So I would like one of two things:
A) Suppress SetNeedsLayout() calls on the surface when one of its Item subviews changes (moves).
--OR --
B) Know why I was asked to relayout, and if it was caused by Item motion then do nothing.
I cannot imagine I am the first to have this question.... :)
###### UPDATE:
After more investigation, I discovered more about what is going on. It is not that moving the Surface's items causes a Surface relayout, as I originally thought. It was only the initiation of a drag which caused the relayout. In digging further I discovered that it wasn't even the drag that was the cause, but a call to the Surface's bringSubviewToFront.
I need to bring the Item to the front, so that when it is dragged it appears on top of the others.
I can understand why bringing a subview might trigger a relayout, but again it is not what I want to happen.
May be you should override layoutSubviews in your Surface UIView.

Resources