UIScrollView Custom Paging - ios

My question has to do with a custom form of paging which I am trying to do with a scroller, and this is easier to visualise if you first consider the type of scroll view implemented in a slot machine.
So say my UIScrollView has a width of 100 pixels. Assume it contains 3 inner views, each with a width of 30 pixels, such that they are separated by a width of 3 pixels. The type of paging which I would like to achieve, is such that each page is one of my views (30 pixels), and not the whole width of the scroll view.
I know that usually, if the view takes up the whole width of the scroll view, and paging is enabled then everything works. However, in my custom paging, I also want surrounding views in the scroll view to be visible as well.
How would I do this?

I just did this for another project. What you need to do is to place the UIScrollView into a custom implementation of UIView. I created a class for this called ExtendedHitAreaViewController. The ExtendedHitAreaView overrides the hitTest function to return its first child object, which will be your scroll view.
Your scroll view should be the page size you want, i.e., 30px with clipsToBounds = NO.
The extended hit area view should be the full size of the area you want to be visible, with clipsToBounds = YES.
Add the scroll view as a subview to the extended hit area view, then add the extended hit area view to your viewcontroller's view.
#implementation ExtendedHitAreaViewContainer
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
if ([[self subviews] count] > 0) {
//force return of first child, if exists
return [[self subviews] objectAtIndex:0];
} else {
return self;
}
}
return nil;
}
#end

Since iOS 5 there is this delegate method: - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset.
So you can do something like this:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint
*)targetContentOffset {
if (scrollView == self.scrollView) {
CGFloat x = targetContentOffset->x;
x = roundf(x / 30.0f) * 30.0f;
targetContentOffset->x = x;
}
}
For higher velocities you might want to adjust your targetContentOffset a bit different if you want a more snappy feeling.

I had the same problem and this worked great for me, tested on iOS 8.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
NSInteger index = lrint(targetContentOffset->x/_pageWidth);
NSInteger currentPage = lrint(scrollView.contentOffset.x/_pageWidth);
if(index == currentPage) {
if(velocity.x > 0)
index++;
else if(velocity.x < 0)
index--;
}
targetContentOffset->x = index * _pageWidth;
}
I had to check the velocity and always go to next/previous page if velocity was not zero, otherwise it would give non-animated jumps when doing very short and fast swipes.
Update: This seems to work even better:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
{
CGFloat index = targetContentOffset->x/channelScrollWidth;
if(velocity.x > 0)
index = ceil(index);
else if(velocity.x < 0)
index = floor(index);
else
index = round(index);
targetContentOffset->x = index * channelScrollWidth;
}
Those are for a horizontal scrollview, use y instead of x for a vertical one.

I've been struggling to overcome this issue, and I found an almost perfect solution which is to ideal with and paging width you want.
I'd set scrollView.isPaging to false (meanwhile, it's false by default) from UIScrollView and set its delegate to UIScrollViewDelegate.
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Stop scrollView sliding:
targetContentOffset.pointee = scrollView.contentOffset
if scrollView == scrollView {
let maxIndex = slides.count - 1
let targetX: CGFloat = scrollView.contentOffset.x + velocity.x * 60.0
var targetIndex = Int(round(Double(targetX / (pageWidth + spacingWidth))))
var additionalWidth: CGFloat = 0
var isOverScrolled = false
if targetIndex <= 0 {
targetIndex = 0
} else {
// in case you want to make page to center of View
// by substract width with this additionalWidth
additionalWidth = 20
}
if targetIndex > maxIndex {
targetIndex = maxIndex
isOverScrolled = true
}
let velocityX = velocity.x
var newOffset = CGPoint(x: (CGFloat(targetIndex) * (self.pageWidth + self.spacingWidth)) - additionalWidth, y: 0)
if velocityX == 0 {
// when velocityX is 0, the jumping animation will occured
// if we don't set targetContentOffset.pointee to new offset
if !isOverScrolled && targetIndex == maxIndex {
newOffset.x = scrollView.contentSize.width - scrollView.frame.width
}
targetContentOffset.pointee = newOffset
}
// Damping equal 1 => no oscillations => decay animation:
UIView.animate(
withDuration: 0.3, delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: velocityX,
options: .allowUserInteraction,
animations: {
scrollView.contentOffset = newOffset
scrollView.layoutIfNeeded()
}, completion: nil)
}
}
slides contains all page views that you have inserted to UIScrollView.

I have many different views inside scroll view with buttons and gesture recognisers.
#picciano's answer didn't work (scroll worked good but buttons and recognisers didn't get touches) for me so I found this solution:
class ExtendedHitAreaView : UIScrollView {
// Your insets
var hitAreaEdgeInset = UIEdgeInsets(top: 0, left: -20, bottom: 0, right: -20)
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
let hitBounds = UIEdgeInsetsInsetRect(bounds, hitAreaEdgeInset)
return CGRectContainsPoint(hitBounds, point)
}
}

After a couple of days of researching and troubleshooting i came up with something that works for me!
First you need to subclass the view that the scrollview is in and override this method with the following:
-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
UIView* child = nil;
if ((child = [super hitTest:point withEvent:event]) == self)
return (UIView *)_calloutCell;
return child;
}
Then all the magic happens in the scrollview delegate methods
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
//_lastOffset is declared in the header file
//#property (nonatomic) CGPoint lastOffset;
_lastOffset = scrollView.contentOffset;
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGPoint currentOffset = scrollView.contentOffset;
CGPoint newOffset = CGPointZero;
if (_lastOffset.x < currentOffset.x) {
// right to left
newOffset.x = _lastOffset.x + 298;
}
else {
// left to right
newOffset.x = _lastOffset.x - 298;
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.5];
[UIView setAnimationDelay:0.0f];
targetContentOffset->x = newOffset.x;
[UIView commitAnimations];
}
You also need to set the scrollview's deceleration rate. I did it in ViewDidLoad
[self.scrollView setDecelerationRate:UIScrollViewDecelerationRateFast];

alcides' solution works perfectly. i just enable / disable the scrolling of the scrollview, whenever i enter scrollviewDidEndDragging and scrollViewWillEndDragging. if the user scrolls several times before the paging animation is finished, the cells are slightly out of alignment.
so i have:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
scrollView.scrollEnabled = NO
CGPoint currentOffset = scrollView.contentOffset;
CGPoint newOffset = CGPointZero;
if (_lastOffset.x < currentOffset.x) {
// right to left
newOffset.x = _lastOffset.x + 298;
}
else {
// left to right
newOffset.x = _lastOffset.x - 298;
}
[UIView animateWithDuration:0.4 animations:^{
targetContentOffset.x = newOffset.x
}];
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView {
scrollView.scrollEnabled = YES
}

Related

Scroll back to the first image when scroll view it's end

I have a scroll view with 11 images . I want when the last image(11) it's showed , so the scroll view is ended , to get back to the first image. Any ideea how can i do this ?
I was tryed with :
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView{
return YES;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.scrollView.contentOffset = CGPointMake(0,0);
}
but i don;t have the result wanted.
Firstly you need to check whether content in the scroll view ended or not. If its ended set its content offset to 0.
CGFloat bottomInset = scrollView.contentInset.bottom;
CGFloat bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height - bottomInset;
if (bottomEdge == scrollView.contentSize.height) {
// Scroll view is scrolled to bottom
[self.scrollView setContentOffset:CGPointMake(0, self.scrollView.contentOffset.y)];
}
use these to check you scrolled to bottom or not..i hop it helps
-(void)scrollViewDidScroll: (UIScrollView*)scrollView
{
float scrollViewHeight = self.scrollView.frame.size.height;
float scrollContentSizeHeight = self.scrollView.contentSize.height;
float scrollOffset = self.scrollView.contentOffset.y;
if (scrollOffset == 0)
{
// then we are at the top
}
else if (scrollOffset + scrollViewHeight == scrollContentSizeHeight)
{
// then we are at the end
[self.scrollView setContentOffset:CGPointMake(0, self.scrollView.contentOffset.y)];
}
}

How to stop a UIScrollView from scrolling when it reaches the bottom

I have the following code in place to grab the event when a UIScrollView reaches then end of its content view:
- (void) scrollViewDidEndDecelerating:(UIScrollView *) scrollView
{
float currentEndPoint = scrollView.contentOffset.y + scrollView.frame.size.height;
// CGPoint bottomOffset = CGPointMake(0, scrollView.contentSize.height - scrollView.bounds.size.height);
// [scrollView setContentOffset:bottomOffset animated:NO];
if (currentEndPoint >= scrollView.contentSize.height)
{
// We are at the bottom
I notice that when I scroll to the bottom it hits it and bounces back up.
If I add this:
CGPoint bottomOffset = CGPointMake(0, scrollView.contentSize.height - scrollView.bounds.size.height);
[scrollView setContentOffset:bottomOffset animated:NO];
then the scroll goes back to the bottom.
Is there any way for it to stay at the bottom without the "bounce back up" as in once it hits the bottom, stop it from moving.
Thanks.
You should uncheck the bounce property of scrollview. Check the screenshot!
I didn't understand much what you mean, what I got that you want to stop scrolling of table view when it reaches to bottom. So here is the process:-
- (void) scrollViewDidEndDecelerating:(UIScrollView *) scrollView
{
float currentEndPoint = scrollView.contentOffset.y + scrollView.frame.size.height;
if (currentEndPoint >= scrollView.contentSize.height)
{
CGPoint offset = scrollView.contentOffset;
offset.x -= 1.0;
offset.y -= 1.0;
[scrollView setContentOffset:offset animated:NO];
offset.x += 1.0;
offset.y += 1.0;
[scrollView setContentOffset:offset animated:NO];
}
}
bounce back problem, u can resolve by
var progressScrollView = UIScrollView()
progressScrollView.bounces = false

UIScrollView scrollViewWillEndDragging: targetContentOffset: not working

Why is this not working? I have a breakpoint set at targetContentOffset->y = -50.0f; and it is being hit, not sure why it's not having any effect.
Yes paging = NO.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
if (scrollView.contentOffset.y < -50.0f) {
targetContentOffset->y = -50.0f;
}
else {
*targetContentOffset = CGPointZero;
// Need to call this subsequently to remove flickering.
dispatch_async(dispatch_get_main_queue(), ^{
[scrollView setContentOffset:CGPointZero animated:YES];
});
}
}
This seems to be your answer.
scrollViewWillEndDragging:withVelocity:targetContentOffset: not working on the edges of a UISCrollView
Theres a UIScrollView bug with setting the targetContentOffset of scrollViewWillEndDragging:withVelocity:targetContentOffset: while the scroll view is at it's default content offset.

UIPanGestureRecognizer interfering with scroll

I have a scroll view with 3 view controllers that I want to swipe between. However, one of my view controllers has a UIPanGestureRecognizer and this stops the scrollview from working. This is the code for the pan:
- (void)pan:(UIPanGestureRecognizer *)aPan; // When pan guesture is recognised
{
CGPoint location = [aPan locationInView:self.view]; // Location of finger on screen
CGRect secondRect = CGRectMake(210.0, 45.0, 70.0, 325.0); // Rectangles of maximimum bar area
CGRect minuteRect = CGRectMake(125.0, 45.0, 70.0, 325.0);
CGRect hourRect = CGRectMake(41.0, 45.0, 70.0, 325.0);
if (CGRectContainsPoint(secondRect, location)) { // If finger is inside the 'second' rectangle
CGPoint currentPoint = [aPan locationInView:self.view];
currentPoint.y -= 80; // Make sure animation doesn't go outside the bars' rectangle
if (currentPoint.y < 0) {
currentPoint.y = 0;
}
else if (currentPoint.y > 239) {
currentPoint.y = 239;
}
currentPoint.y = 239.0 - currentPoint.y;
CGFloat pointy = currentPoint.y - fmod(currentPoint.y, 4.0);
[UIView animateWithDuration:0.01f // Animate the bars to rise as the finger moves up and down
animations:^{
CGRect oldFrame = secondBar.frame;
secondBar.frame = CGRectMake(oldFrame.origin.x, (oldFrame.origin.y - (pointy - secondBar.frame.size.height)), oldFrame.size.width, (pointy));
}];
CGFloat result = secondBar.frame.size.height - fmod(secondBar.frame.size.height, 4.0);
secondInt = (result / 4.0); // Update labels with new time
self->secondLabel.text = [NSString stringWithFormat:#"%02d", secondInt];
}
}
Basically, this code allows the user to drag their finger up and down the screen, and change the height of a uiview. Therefore, I only need the up/down pan gesture for this and only the left/right pan gestures for the scroll view.
Does anyone have any idea how I can do this?
Your scrollview has a property called panGestureRecognizer. You can create a delegate for your own UIPanGestureRecognizer, and implement this method:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
if(otherGestureRecognizer == myScrollView.panGestureRecognizer) {
return YES;
} else {
return NO;
}
}
UIPanGestureRecognizer contains a delegate method - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch that you can return YES or NO to let/prevent the pan gesture from receiving the touch.
Within that method, you could write something like:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
CGPoint velocity = [gestureRecognizer velocityInView:yourView];
if(velocity.x > 0)
{
NSLog(#"gesture went right"); // return NO
}
else if (velocity.x < 0)
{
NSLog(#"gesture went left"); // return NO
}
if (velocity.y > 0)
{
NSLog(#"gesture went down"); // return YES
}
else if (velocity.y < 0)
{
NSLog(#"gesture went up"); // return YES
}
}
Admittedly, that probably isn't the cleanest implementation. You're leaving an open door for mixed gestures. So just make sure to watch for that.

Paging in Collection View not centering properly

I have a UICollectionView with paging enabled and the individual views that are moving horizontally are not getting centered properly.
I have made sure that the views are the same width as the screen.
Any pointers on how to force the UICollectionView to page the views horizontally?
You have to make sure that your section inset + line spacing + cell width all equals exactly the bounds of the CollectionView. I use the following method on a custom UICollectionViewFlowLayout sublcass:
- (void)adjustSpacingForBounds:(CGRect)newBounds {
NSInteger count = newBounds.size.width / self.itemSize.width - 1;
CGFloat spacing = (newBounds.size.width - (self.itemSize.width * count)) / count;
self.minimumLineSpacing = spacing;
UIEdgeInsets insets = self.sectionInset;
insets.left = spacing/2.0f;
self.sectionInset = insets;
}
You have to remember that UICollectionView is just a sublcass of UIScrollView so it doesn't know how you want your pagination to work. Before UICollectionView you would have to handle all of this math when laying out your subviews of the UIScrollView.
Override the method:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
*targetContentOffset = scrollView.contentOffset; // set acceleration to 0.0
float pageWidth = (float)self.articlesCollectionView.bounds.size.width;
int minSpace = 10;
int cellToSwipe = (scrollView.contentOffset.x)/(pageWidth + minSpace) + 0.5; // cell width + min spacing for lines
if (cellToSwipe < 0) {
cellToSwipe = 0;
} else if (cellToSwipe >= self.articles.count) {
cellToSwipe = self.articles.count - 1;
}
[self.articlesCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:cellToSwipe inSection:0] atScrollPosition:UICollectionViewScrollPositionLeft animated:YES];
}

Resources