Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Smooth custom paging for UIScrollView

Tags:

ios

swift

I have two (possibly more) views in a UIScrollView and want to use paging with it. The problem arises when I try to use the default Paging option for UIScrollView, since the views have different widths it can not page properly.

So I have implemented a custom paging code which works. However, when the scrolls are slow, it does not function as expected. (It goes back to the original position without animation.)

Here is how I currently do the custom paging through the UIScrollViewDelegate

func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
    if direction == 1{
        targetContentOffset.pointee.x = 0
    }else{
        targetContentOffset.pointee.x = 100
    }
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
    if scrollView.panGestureRecognizer.translation(in: scrollView.superview).x > 0 {
        direction = 1
    }
    else {
        direction = 0
    }
}

What I want: enter image description here What I have: enter image description here

like image 789
Taichi Kato Avatar asked Sep 10 '17 08:09

Taichi Kato


2 Answers

try to below example for Custom UIScrollView Class

import UIKit

public class BaseScrollViewController: UIViewController, UIScrollViewDelegate {

    public var leftVc: UIViewController!
    public var middleVc: UIViewController!
    public var rightVc: UIViewController!

    public var initialContentOffset = CGPoint() // scrollView initial offset
    public var maximumWidthFirstView : CGFloat = 0

    public var scrollView: UIScrollView!

    public class func containerViewWith(_ leftVC: UIViewController,
                                        middleVC: UIViewController,
                                        rightVC: UIViewController) -> BaseScrollViewViewController {
        let container = BaseScrollViewViewController()

        container.leftVc = leftVC
        container.middleVc = middleVC
        container.rightVc = rightVC
        return container
    }

    override public func viewDidLoad() {
        super.viewDidLoad()
        setupHorizontalScrollView()
    }

    func setupHorizontalScrollView() {
        scrollView = UIScrollView()
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.bounces = false

        let view = (
            x: self.view.bounds.origin.x,
            y: self.view.bounds.origin.y,
            width: self.view.bounds.width,
            height: self.view.bounds.height
        )

        scrollView.frame = CGRect(x: view.x,
                                  y: view.y,
                                  width: view.width,
                                  height: view.height
        )

        self.view.addSubview(scrollView)

        let scrollWidth  = 3 * view.width
        let scrollHeight  = view.height
        scrollView.contentSize = CGSize(width: scrollWidth, height: scrollHeight)

        leftVc.view.frame = CGRect(x: 0,
                                   y: 0,
                                   width: view.width,
                                   height: view.height
        )

        middleVc.view.frame = CGRect(x: view.width,
                                               y: 0,
                                               width: view.width,
                                               height: view.height
        )

        rightVc.view.frame = CGRect(x: 2 * view.width,
                                    y: 0,
                                    width: view.width,
                                    height: view.height
        )

        addChildViewController(leftVc)
        addChildViewController(middleVc)
        addChildViewController(rightVc)

        scrollView.addSubview(leftVc.view)
        scrollView.addSubview(middleVc.view)
        scrollView.addSubview(rightVc.view)

        leftVc.didMove(toParentViewController: self)
        middleVc.didMove(toParentViewController: self)
        rightVc.didMove(toParentViewController: self)

        scrollView.contentOffset.x = middleVc.view.frame.origin.x
        scrollView.delegate = self
    }


    public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        self.initialContentOffset = scrollView.contentOffset
    }

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if maximumWidthFirstView != 0
        {
            if scrollView.contentOffset.x < maximumWidthFirstView
            {
                scrollView.isScrollEnabled = false
                let newOffset = CGPoint(x: maximumWidthFirstView, y: self.initialContentOffset.y)

                self.scrollView!.setContentOffset(newOffset, animated:  false)
                scrollView.isScrollEnabled = true

            }
        }

    }

}

Use of BaseScrollViewController

let left = FirstController.init()
let middle = MiddleController()
let right = RightController.init()

let container = BaseScrollViewController.containerViewWith(left,middleVC: middle,rightVC: right)
container.maximumWidthFirstView = 150

Output:

simulator screen shot 14-sep-2017 12 57 09 pmsimulator screen shot 14-sep-2017 12 57 05 pmsimulator screen shot 14-sep-2017 12 59 38 pm

GitHub gist Example code: https://gist.github.com/mspvirajpatel/58dac2fae0d3b4077a0cb6122def6570

like image 158
Virajkumar Patel Avatar answered Oct 23 '22 01:10

Virajkumar Patel


I have previously written a short memo about this problem, and I'll copy/paste it since it is no longer accessible from anywhere. This may not be a specific answer and the codes are pretty old, but I hope this would help you in some degree.


If you have used a paging feature included in UIScrollView, you might also have tempted to customize the width of each page instead of a default, boring, frame width paging. It would be great if you can make the scroll stop at shorter or longer intervals than just multiples of its frame width. Surprisingly, there's no built-in way to configure the width of pages even in our latest iOS7 SDK. There are some ways to achieve custom paging, but none of them I would say are complete. As for now, you'll have to choose either of the following solutions.

1. Change the frame size of your UIScrollView

Alexander Repty has introduced a nice and easy solution to this problem and also included a sample code through his blog: http://blog.proculo.de/archives/180-Paging-enabled-UIScrollView-With-Previews.html

Basically, the instruction can be watered down to the following steps:

  1. Create UIView subclass and override hitTest: withEvent:.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if ([self pointInside:point withEvent:event]) {
        if ([self.subviews count] == 0) return nil;
        else return [self.subviews lastObject];
    }
    return nil;
}
  1. Include UIScrollView as a subview of the above UIView subclass.

  2. Adjust the frame size of your UIScrollView.

  3. Set clipsToBound property of your scroll view to NO.

  4. Set pagingEnabled property of your scroll view to YES.

As you can see, I've just assumed that there is only one subview (the scrollView!) to your UIView subclass. Since you are passing all the touch events occurred in the UIView subclass to your UIScrollView, you'll be able to scroll the content by panning on the UIView subclass, but the paging width will be decided by the width of UIScrollView's frame.

The best part of this approach is that you'll get the genuine feeling and responsiveness, as it is somewhat hard to mimic the paging by using UIScrollView delegate methods.

The only problem I found using this solution is that the width of all pages will have to be identical. You can't set different widths to different pages. If you tries to change your scrollView's frame size dynamically, you'll find there're a number of new emerging problems to deal with. Before trying to fix these glitches, you may want to check out other two solutions using UIScrollView delegates.

2. scrollViewWillEndDragging: withVelocity: targetContentOffset

scrollViewWillEndDragging: withVelocity: targetContentOffset is one of the latest UIScrollView delegate methods(iOS 5.0 or up) that gives you more information than the other old ones.

Since you get the velocity of the scrollView right after you lift the finger up from the screen, we can figure out the direction of the scrolled contents. The last argument, targetContentOffset, not only gives you the expected offset when the scrolling stops eventually, you can also assign CGPoint value in order to let the scrollView scrolls to the desired point.

targetContentOffset = CGPointMake(500, 0);

or

targetContentOffset->x = 500;

However, this will not work as you would think it should because you cannot set the speed of scrolling animation. It feels more like the scrollView happens to stop at the right point rather than it snaps to the spot. I also have to warn you that manually scrolling the contents with setContentOffset: animated: or just by using UIView animation inside the method will not work as expected.

If the velocity is 0, however, you may(and you have to) use manual scrolling to make it snap to the nearest paging point.

It could be the simplest and the most clean approach among all, but the major downside is that it does not provide the same experience that you always had with the real paging feature. To be more honest, it's not even similar to what we call paging. For the better result, we need to combine more delegate methods.

3. Use multiple UIScrollView delegate methods

From my shallow experience, an attempt to scroll your scrollView manually inside any UIScrollView delegate methods will only work when your scrollView has started to decelerate, or when it's not scrolling at all. Therefore, the best place I've found to perform the manual scrolling is scrollViewWillBeginDecelerating:.

Before looking inside the sample code, remember scrollViewEndDragging: withVelocity: targetContentOffset: method will always called prior to scrollViewWillBeginDecelerating:.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    _scrollVelocity = velocity.x;
    if (_scrollVelocity == 0) {
        // Find the nearest paging point and scroll.
    }
}

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
{
    if (_scrollVelocity < 0) {
        [UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
        scrollView.contentOffset = // Previous page offset
        } completion:^(BOOL finished){}];
    } else if (_scrollVelocity > 0) {
        // Animate to the next page offset
    }
}

_scrollVelocity is meant to be a global variable or a property, and I've assumed that you have your own ways to decide paging offsets for each page. Note that you'll have to handle the case of zero velocity inside the upper method because the latter method will not be called.

UIView animation with the duration 0.3 and the EaseOut curve option gave me the best result, but of course you should try other combinations to find what's the best for you.

like image 2
sCha Avatar answered Oct 23 '22 00:10

sCha