When using D3 on IOS (tested on versions 8.1, 10.3.1, and 11.4), any transformations called after a .transition() within a scroll listener do are not applied until the scroll has completed. For example, if the following were inside a scroll listener function, the height would not be set until the scroll finished:
d3.select('.my-rect')
.transition()
.attr('height', () => {
// assume `date` updated by scroll listener
return myTimeScale(date);
});
If I remove the .transition() then everything works as expected.
If I put in console.log statements, it's clear that the myTimeScale(date) is being executed on every scroll/touchmove event, but nothing changes visually until the scroll has concluded and the user lifts their finger from the screen.
A couple important things to note here:
This is happening even when listening for both touchmove and scroll events
This does not appear to be a problem relating to scroll inertia because nothing is updating even with a completely controlled scroll
I can work around this problem, but curious if anybody knows what is causing it.
Related
Can I know in advance where the Scrollview will stop before scrolling stop? I have consulted relevant online materials, but it seems that there is no relevant topic. I am making a magic app to know where it will scroll before Scrollview stops, so that I can modify the value there in advance
My attempt: mark when it starts to slow down and stop, but the sliding distance between them is uncertain, so I haven't finished my idea yet
There is a scroll view delegate method.
https://developer.apple.com/documentation/uikit/uiscrollviewdelegate/1619385-scrollviewwillenddragging
This will tell you the intended target offset when the deceleration finishes.
It is intended to be used to change the target offset. For instance if you want to make sure it aligns with the content you have tween it finishes.
But you don’t have to change it. You can just return the target offset but use that to change the content like you mentioned.
I have a horizontally scrolled UICollectionView with a title label above it and a UIPageControl below it.
UILabel
UICollectionView
UIPageControl
When I turn on the VoiceOver accessibility feature and start traversing the screen sequentially, the collection view scrolls to the beginning or end automatically. Making the page jump off suddenly. For example, if I scroll to the 2nd page using page control, and move back to the collection view, it shows and reads the last page unexpectedly. Since I'm using the page control for navigation in the accessibility mode, I'd like to prevent the automatic scrolling.
How do I prevent or counter that?
I found an issue that seems to describe the same problem, but there's no workaround suggestion: iOS 8.4: Scroll view resets contentOffset with Voice Over enabled shortly after view appear
I encountered it on iOS 13.4.1 iPhone 11 Pro
UIScrollViewDelegate.scrollViewDidScroll(_:)
A change in the accessibility focus that triggers an automatic scrolling also triggers a call to scrollViewDidScroll(_:) in your UIScrollViewDelegate. Use that to counter the automatic scrolling effect, f.i. by setting contentOffset the way you prefer it.
You may need to detect that the scrolling was actually triggered by accessibility features, and not the user dragging or pinching. UIAccessibility.isVoiceOverRunning and UIAccessibilityFocus.accessibilityElementDidBecomeFocused() are your friends here. Beware that changing contentOffset (or zoomScale or whatever is needed) may trigger another call to scrollViewDidScroll(_:), so you need to prevent an infinite recursion.
Using #pommy's suggestions, I was able to fix my similar issue. In the code I was working on, the most appropriate place to make the change ended up being CalendarCollectionView.setContentOffset(_:animated:), where CalendarCollectionView is a UICollectionView subclass. Specifically, it's a JTACMonthView subclass, but that should not be of any relevance to this answer.
From the name, you can see my use case: a calendar which shows a month at a time. It could have many months both into the future and the past, but the usual user focus is likely to start somewhere in the middle.
Like the OP, I found that swiping from an outer element to the collection view with VoiceOver enabled caused the focus to go to the first date in the calendar, in my case 1st January 1951 (Date.farPast, I believe.) An interesting aside: Switch Control navigation did not cause the same behaviour.
The underlying behaviour was that contentOffset was getting set to 0.0 in the dimension that the collection view scrolls. In my code, that direction is held in style, and changes based on configuration, but in most applications it's likely to be fixed.
My code simply blocks any offset changes to 0.0 when VoiceOver is enabled. This is pretty naïve, and won't be suitable for all apps, but gives a concrete example which I hope will help some others!
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
if shouldPreventAccessibilityFocusScrollback(for: contentOffset) {
return
}
super.setContentOffset(contentOffset, animated: animated)
}
func shouldPreventAccessibilityFocusScrollback(for newContentOffset: CGPoint) -> Bool {
if UIAccessibility.isVoiceOverRunning {
switch style {
case .horizontal:
return newContentOffset.x == 0
case .vertical:
return newContentOffset.y == 0
}
}
return false
}
I spent quite a long time trying to determine when UIAccessibilityFocus moved from something outside the collection view, to something inside the collection view, which is ideally the only time we want to block these automatic scrolls. I was unsuccessful, but I think that was mostly due to subclassing a third party collection view (the calendar). There's definitely more merit to that approach, if you can get it to work... but it will require some careful management of state.
I am doing paging effect in UICollectionView. My solution is shown below.
setContentOffset method will be called in scrollViewWillEndDragging and calculate the next or previous page contentOffset x value and set it with animation.
However, there is one issue which is that once the setContentOffset function has been called, if I touch the screen, then the scrollView will be stopped. Even if you release your finger, it won't continue, which means it stops at a wrong position.
Actually, I've tried to override the targetOffset in UICollectionViewFlowLayout but this issue still exists. Also, I tried to call touchesEnded but this is not even triggered at all. Furthermore, I tried isPagingEnabled and it won't cause this issue but my collectionView items are more complicated, which leads to a wrong targetContentOffset.
My current solution is set scrollView.isUserInteractionEnabled = false after setContentOffset and set it back to true when scrollViewDidEndScrollingAnimation called. This is okay but I am still wondering if there is any good way to do this?
I tried both Google Calendar and Outlook, they will reset you back to the position it should be.
I searched online and I cannot find any questions regarding this issue.
Could you help me? Thanks!
I tried a lot to figure it out and finally, I got something correct to share with you guys.
The solution is to set targetContentOffset in scrollviewWillEndDragging, then I can get what I want.
The truth behind this is that if you call setContentOffset, then scrollviewWillEndDragging won't be called in the second time endDragging (when you touch the screen after the first endDragging). However, if you simply set targetContentOffset = requiredContentOffset, then the second time endDragging will be called and at this time, the paging method will be called again to navigate UIScrollView to a correct position.
What I've learned from this is never call setContentOffset when you do the pagination effect. Some tutorials online for the pagination are totally wrong.
(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.
I'm using Ember-Gestures which implements Hammer.js in a Cordova app to implement some simple gesture controls.
I'm running into a major problem whereby any gesture that triggers an animation (transition, transform, SVG animations), if the screen is scrolling any amount, that animation will freeze and be at its end state when scrolling is complete. In particular, I have an element on a vertically scrollable page which should (ideally) be able to be pinched in and out to expand it into multiple elements or back into one.
I'm familiar that as an optimization iOS freezes all animation during scroll. However, since pinch and swipe gestures can both slightly scroll the screen, this is terrible for user experience because elaborate transitions can be completely frozen if the user swipes, for example, slightly up and to the left rather than just directly left.
I've tried a few solutions to enable rendering during scroll like those here, but these don't seem to work on contemporary versions of iOS. I've also tried the hammerJS e.preventDefault() method to freeze scrolling during gestures called through the Ember-gestures extension, so my method looks like:
swipeLeft(e) {
e.originalEvent.gesture.srcEvent.preventDefault()
// Do stuff
},
...but this doesn't have any appreciable effect. (Maybe there's something wrong here? gesture had no preventDefault() method itself, and ember-gestures seems to try to abstract some of this away.
Is there any way I can either keep animations rendering during scrolling (this seems unlikely), or alternately STOP a page from scrolling right before performing an animation (and prevent scroll while it's executing)?
Alternately is there any way I can add constraints to what is interpreted as a "pinch" or "swipe" gesture such that those that would also be interpreted as scroll gestures are excluded.
My solution here ended up being to add event handlers such that when the screen is touched with multiple fingers, the body is set to fixed position such that it's unscrollable for the duration of the touch (with the fixed position removed when touch is ended). I added the handlers to the pinchStart and pinchEnd events
I suspect there might be a more elegant solution out there, but for the purpose of disabling accidental scrolling while pinching so that D3.js animations won't freeze midway, this was a quick and effective fix.