I am trying to implement is "snap to cell" effect in my UITableView.
My UITableView has 3 equally sized cells, but for some reason the UITableView always snaps to cell 0 or cell 1 and doesn't ever snap to the third cell. its also wrong by about 50 points too low, no matter how hard you slide. I use the most obvious code to create the effect so I can't understand why it doesn't work.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSIndexPath *pathForTargetTopCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), targetContentOffset->y)];
DLog(#"indexPathForRow %d", pathForTargetTopCell.row);
targetContentOffset->y = [self.tableView rectForRowAtIndexPath:pathForTargetTopCell].origin.y;
}
Any thoughts about what could be going wrong?
EDIT:
I suspect this has something to do with the status bar and nav bar on top, since its off by exactly 64 points. Still doesn't explain why it doesn't recognise the last cell though..
For the status bar offset, do this in your VC's viewDidLoad:
if([self respondsToSelector:#selector(setAutomaticallyAdjustsScrollViewInsets:)])
[self setAutomaticallyAdjustsScrollViewInsets:NO];
edit
Just for testing, could you add 4 equally size cells and try snapping to the 3rd and 4th cells?
Modify your scrollViewWillEndDragging implementation as follows:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSIndexPath *pathForCenterCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), CGRectGetMidY(self.tableView.bounds))];
[self.tableView scrollToRowAtIndexPath:pathForCenterCell atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
}
Related
I have two UICollectionViews and when the user scrolls on one, the other should also change by scrolling to the same index path. I've implemented the following code so that, when the first UICollectionView decelerates and lands on one cell, the second UICollectionView should automatically scroll to that same IndexPath:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
// Only do this for the first UICollectionView
if (scrollView.tag == 0) {
// Find the new IndexPath
CGRect visibleRect = (CGRect){.origin = self.filterCollectionView.contentOffset, .size = self.filterCollectionView.bounds.size};
CGPoint visiblePoint = CGPointMake(CGRectGetMidX(visibleRect), CGRectGetMidY(visibleRect));
NSIndexPath *visibleIndexPath = [self.filterCollectionView indexPathForItemAtPoint:visiblePoint];
// On main thread, scroll second CollectionView to visibleIndexPath
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Row: %li, Sec: %li", visibleIndexPath.row, visibleIndexPath.section);
[self.filterTitleCollectionView reloadData];
[self.filterTitleCollectionView layoutIfNeeded];
[self.filterTitleCollectionView setNeedsDisplay];
//scroll titles to same index path
[self.filterTitleCollectionView scrollToItemAtIndexPath:visibleIndexPath atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
NSLog(#"Row: %li, Sec: %li", self.filterTitleCollectionView.indexPathsForSelectedItems[0].row, self.filterTitleCollectionView.indexPathsForSelectedItems[0].section);
});
}
}
For some reason, the filterTitleCollectionView does not scroll to the visibleIndexPath. Or if it does scroll, it scrolls to the wrong cell (in a predictable pattern). You can see the video of it here:
https://streamable.com/q2lnnw
The row number is in the title of the cell. As you can see, the second UICollectionView keeps scrolling to row 2 for some reason.
The correct IndexPath is printed, but it scrolls to the wrong IndexPath or doesn't scroll at all.
I'm trying to make my cells snap into place, where the height of my cells is UITableViewCell height = self.view.frame.size.height - 80
I'm using this code to snap them. This works when scrolling upward, but when I scroll downward I run into a problem where if I completely drag the next cell onto the screen, it will snap back to the current cell, even if I scroll so far that only a little bit of the current cell is still showing. Anybody know what I'm doing wrong?
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSIndexPath *pathForTargetTopCell = [self.purchasesTableview indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.purchasesTableview.bounds), targetContentOffset->y)];
targetContentOffset->y = [self.purchasesTableview rectForRowAtIndexPath:pathForTargetTopCell].origin.y;
}
A sample UITableViewCell from my UITableView
You don't need any code to make your screen-sized cells snap into place, you can do it by selecting the "Paging Enabled" check box in IB for the table view.
I want to implement an UITableView with some animation while the user is scrolling. Only the first visible (top most) cell has to be an different layout. Before the second cell reaches the top of UITableView I want to start the small animation. (changing background alpha and width of cells contentView etc.)
How could I handle this?
[UPDATE]
What I have tried so far:
using UIScrollView delegate method scrollViewDidScroll:(UIScrollView *)scrollView
together with UITableView´s visibleCells property using the first item of it to get the
reference with no luck
This is what I have done:
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
if (!decelerate) {
[self scrollingFinish];
}
}
..
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self scrollingFinish];
}
..
- (void)scrollingFinish {
NSIndexPath *firstVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] objectAtIndex:0];
NSLog(#"first visible cell's section: %i, row: %i", (int) firstVisibleIndexPath.section, (int) firstVisibleIndexPath.row);
APFCategoryCell * cell = (APFCategoryCell*) [self.tableView cellForRowAtIndexPath:firstVisibleIndexPath];
[cell animateCell];
[self.tableView scrollToRowAtIndexPath:firstVisibleIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
The problem appears, if the user is scrolling down because the animated cell is still visible and the next one gets animated. So two or more cells are animated. I could going on and implement an method in my custom cell like - (void) resetAnimatedCell but maybe there is an much more elegant way for doing this?
How would you go about implementing a UICollectionView with this behaviour?
The idea is that once a user navigates past a certain point, they cannot go back and view those cells again.
My attempt at a solution has been to listen for gestures on the collection view and if disable scrolling once a swipe occurs on the element. The obvious problem with this is that the user can simple hold and drag any particular cell.
Any thoughts?
I think this behavior may be confusing for your users.
Maybe you should try to add some elasticity/bouncing so that your users would be less confused.
Anyway, I see two different ways to achieve this without subclassing
1/ Since UICollectionViewDelegate conforms to UIScrollViewDelegate, you can get the starting offset of your scrollview with – scrollViewWillBeginDragging: then in – scrollViewDidScroll: you would compare the new offset's x value. If the new offset.x 's value is smaller than the starting one, set it to 0 and update your scrollview.
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
offset = scrollView.contentOffset;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint newOffset = scrollView.contentOffset;
if (newOffset.x < offset.x) {
// scrolling to the left, reset offset
[scrollView setContentOffset:offset];
}
}
Because there is inertia with scrolling in iOS, scrollViewDidScroll: is called a lot of time, so it may cause performance issues. You may reduce the number of call by targeting your offset with scrollViewWillEndDragging:withVelocity:targetContentOffset: from UIScrollViewDelegate.
2/ Or ou can just use the method scrollViewWillEndDragging:withVelocity:targetContentOffset: which I just spoke about, which sets the offset back to its beginning, with an animation.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGPoint newOffset = scrollView.contentOffset;
if (newOffset.x < offset.x) {
// scrolling to the left, reset offset with animation
targetContentOffset->x = offset.x;
}
}
3/ You spoke of UISwipeGestureRecognizer, did you give a try to UIPanGestureRecognizer? This is what "simple hold and drag" is.
You can implement it the same way you would implement an infinite scroll view, by adding / removing items based on the scroll offset.
Override the viewDidScroll: method (UICollectionViewDelegate)
Check if your offset puts the first object of your list past the offset (ie. can you still see it on the screen?)
If it is, then remove it from the collection view.
This simple implementation might result in choppy animations, you might have to do some optimization once it's working but this should get you started.
Another possible solution would be to "reposition" all your elements constantly to appear where they were before you started scrolling if you are scrolling left.
You can achieve this by keeping track of the highest offsetX you ever encountered, and reposition your cells if the current offsetX is lower than the max. That way you will have the impression that your cells are not moving or that you can't scroll, but you will actually be scrolling.
In my case, I have a paginated collection view, so I have to take care when decelerating in the last item of the collection view as well in automatic decelerations for each page.
To fix the issue, I just disable user interaction while the collection view is "moving automatically".
Here the code:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (!_isDragging) // <-- only do things if the user is dragging!
return;
CGPoint contentOffset = scrollView.contentOffset;
// If we move to the left
if (contentOffset.x < _contentOffset.x)
{
CGSize contentSize = scrollView.contentSize;
// If content offset is moving inside contentSize:
if ((contentOffset.x + scrollView.bounds.size.width) < contentSize.width)
scrollView.contentOffset= _contentOffset;
}
else
{
// Update the current content offset
_contentOffset = contentOffset;
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_isDragging = YES;
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
_isDragging = NO;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (decelerate)
{
// If willDecelerate, stop user interaction!
_collectionView.userInteractionEnabled = NO;
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
// Once deceleration is finished, enbale user interaction
_collectionView.userInteractionEnabled = YES;
// Set the new content offset
_contentOffset = scrollView.contentOffset;
}
I want to revert my UIScrollView's content offset if I don't drag enough:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(CGPoint *)targetContentOffset {
self.endingOffset = scrollView.contentOffset;
if(abs(verticalOffset) > [self cellHeight] / 9) { // If the user scrolled enough distance, attempt to scroll to the next cell
...
} else if(self.nextCellOrigin.y != 0) { // The scroll view is still scrolling and the user didn't drag enough
...
} else { // If the user didn't drag enough
self.tableView.decelerationRate = UIScrollViewDecelerationRateNormal;
(*targetContentOffset) = self.startingOffset;
}
}
The code to revert to the original position is in the else portion, and it always works. However, when I don't scroll enough and make the gesture quickly, it snaps back. If I scroll just a little and then hold that position for slightly longer than usual, it reverts back smoothly.
I haven't found anything in the API reference for how long a user has touched a UIScrollView, and even if I did it's not immediately obvious how I could use that to change the behavior of my reverting code. I've also tried scrolling to the position with setContentOffset:animated: but that doesn't seem to fix the jerkiness.
Any ideas?
Have you tried logging the velocity to find out how it is when the jerkiness happens?
EDIT:
What you can try to do is implement these two scrollView delegate methods instead of the willEndDragging method. This solution will give a different feeling to the scrollView, but give it a try.
Fill the checkOffset method with all the logic you need.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// if scrollView's contentOffset reached its final position during scroll, it will not decelerate, so you need a check here too
if (!decelerate) {
[self checkOffset];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self checkOffset];
}
- (void)checkOffset {
CGPoint newOffset;
...
// Do all the logic you need to move the content offset, then:
...
[self.scrollView setContentOffset:newOffset animated:YES];
}
EDIT #2:
Maybe you could achieve a better result if you also add this to my solution.. Try ;)
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(CGPoint *)targetContentOffset {
// This should force the scrollView to stop its inertial deceleration, by forcing it to stop at the current content offset
*targetContentOffset = scrollView.contentOffset;
}