46

I have a few screens worth of content within my UIScrollView which only scrolls vertically.

I want to programmatically scroll to a view contained somewhere in it's hierarchy.

The UIScrollView move so that the child view is at the top of the UIScrollView (either animated or not)

dyson returns
  • 3,376
  • 4
  • 16
  • 20

11 Answers11

121

Here's an extension I ended up writing.

Usage:

Called from my viewController, self.scrollView is an outlet to the UIScrollView and self.commentsHeader is a view within it, near the bottom:

self.scrollView.scrollToView(self.commentsHeader, animated: true)

Code:

You only need the scrollToView method, but leaving in scrollToBottom / scrollToTop methods too as you'll probably need those too, but feel free to delete them.

extension UIScrollView {

    // Scroll to a specific view so that it's top is at the top our scrollview
    func scrollToView(view:UIView, animated: Bool) {
        if let origin = view.superview {
            // Get the Y position of your child view
            let childStartPoint = origin.convertPoint(view.frame.origin, toView: self)
            // Scroll to a rectangle starting at the Y of your subview, with a height of the scrollview
            self.scrollRectToVisible(CGRect(x:0, y:childStartPoint.y,width: 1,height: self.frame.height), animated: animated)
        }
    }

    // Bonus: Scroll to top
    func scrollToTop(animated: Bool) {
        let topOffset = CGPoint(x: 0, y: -contentInset.top)
        setContentOffset(topOffset, animated: animated)
    }

    // Bonus: Scroll to bottom
    func scrollToBottom() {
        let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
        if(bottomOffset.y > 0) {
            setContentOffset(bottomOffset, animated: true)
        }
    }

}
AMAN77
  • 6,218
  • 9
  • 45
  • 60
dyson returns
  • 3,376
  • 4
  • 16
  • 20
  • 27
    This is not working well for me if the keyboard is on screen. Instead of using scrollRectToVisible i'm using: setContentOffset(CGPoint(x: 0, y: childStartPoint.y), animated: animated) – Nati Jan 16 '17 at 08:18
  • 1
    self.scrollRectToVisible(CGRect(x: 0, y: childStartPoint.y, width: 1, height: view.frame.height), animated: animated) worked for me. Used view height – ChanOnly123 Jul 04 '18 at 05:11
  • consider replacing `height: self.frame.height` with `height: frame.height - (contentInset.top + contentInset.bottom)` when using content insets. – Lloyd Keijzer Jul 18 '19 at 08:57
  • @Nati When the keyboard is on screen you need to ensure that UIScrollView's contentInset property is set correctly to account for the area under the keyboard. – Darren Sep 27 '21 at 20:49
  • Replacing ```let childStartPoint = origin.convertPoint(view.frame.origin, toView: self)``` to ```let childStartPoint = origin.convert(view.frame.origin, to: self)``` solved my problem. – Md. Arif Oct 27 '21 at 12:41
8
 scrollView.scrollRectToVisible(CGRect(x: x, y: y, width: 1, height:
1), animated: true)

or

scrollView.setContentOffset(CGPoint(x: x, y: y), animated: true)

Another way is

scrollView.contentOffset = CGPointMake(x,y);

and i do it with animated like this

[UIView animateWithDuration:2.0f delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
scrollView.contentOffset = CGPointMake(x, y); }
completion:NULL];
Amr Angry
  • 3,711
  • 1
  • 45
  • 37
4
scrollView.setContentOffset(CGPoint, animated: Bool)

Where the point's y coordinate is the y coordinate of the frame of the view you want to show relatively to the scrollView's content view.

Max Pevsner
  • 4,098
  • 2
  • 18
  • 32
3

Here is my answer, this is in swift. This will scroll the pages in scrollview infinitely.

private func startBannerSlideShow()
{
UIView.animate(withDuration: 6, delay: 0.1, options: .allowUserInteraction, animations: {
    scrollviewOutlt.contentOffset.x = (scrollviewOutlt.contentOffset.x == scrollviewOutlt.bounds.width*2) ? 0 : scrollviewOutlt.contentOffset.x+scrollviewOutlt.bounds.width
}, completion: { (status) in
    self.startBannerSlideShow()
})
}
Rahul K Rajan
  • 776
  • 10
  • 19
3

Updated dyson's answer to behave like UITableView's scrollToRowAtIndexPath:atScrollPosition:animated: since that was my use case:

extension UIScrollView {
    
    /// Scrolls to a subview of the current `UIScrollView `.
    /// - Parameters:
    ///   - view: The subview  to which it should scroll to.
    ///   - position: A constant that identifies a relative position in the `UIScrollView ` (top, middle, bottom) for the subview when scrolling concludes. See UITableViewScrollPosition for descriptions of valid constants.
    ///   - animated: `true` if you want to animate the change in position; `false` if it should be immediate.
    func scrollToView(view: UIView,
                      position: UITableView.ScrollPosition = .top,
                      animated: Bool) {
        
        // Position 'None' should not scroll view to top if visible like in UITableView
        if position == .none &&
            bounds.intersects(view.frame) {
            return
        }
        
        if let origin = view.superview {
            // Get the subview's start point relative to the current UIScrollView
            let childStartPoint = origin.convert(view.frame.origin,
                                                 to: self)
            var scrollPointY: CGFloat
            switch position {
            case .bottom:
                let childEndY = childStartPoint.y + view.frame.height
                scrollPointY = CGFloat.maximum(childEndY - frame.size.height, 0)
            case .middle:
                let childCenterY = childStartPoint.y + view.frame.height / 2.0
                let scrollViewCenterY = frame.size.height / 2.0
                scrollPointY = CGFloat.maximum(childCenterY - scrollViewCenterY, 0)
            default:
                // Scroll to top
                scrollPointY = childStartPoint.y
            }

            // Scroll to the calculated Y point
            scrollRectToVisible(CGRect(x: 0,
                                       y: scrollPointY,
                                       width: 1,
                                       height: frame.height),
                                animated: animated)
        }
    }

    
    /// Scrolls to the top of the current `UIScrollView`.
    /// - Parameter animated: `true` if you want to animate the change in position; `false` if it should be immediate.
    func scrollToTop(animated: Bool) {
        let topOffset = CGPoint(x: 0, y: -contentInset.top)
        setContentOffset(topOffset, animated: animated)
    }

    /// Scrolls to the bottom of the current `UIScrollView`.
    /// - Parameter animated: `true` if you want to animate the change in position; `false` if it should be immediate.
    func scrollToBottom(animated: Bool) {
        let bottomOffset = CGPoint(x: 0,
                                   y: contentSize.height - bounds.size.height + contentInset.bottom)
        if (bottomOffset.y > 0) {
            setContentOffset(bottomOffset, animated: animated)
        }
    }
}
Bogdan
  • 402
  • 2
  • 8
  • 18
2

swift 5.0 code

extension UIScrollView {

    // Scroll to a specific view so that it's top is at the top our scrollview
    func scrollToView(view:UIView, animated: Bool) {
        if let origin = view.superview {
            // Get the Y position of your child view
            let childStartPoint = origin.convert(view.frame.origin, to: self)
            // Scroll to a rectangle starting at the Y of your subview, with a height of the scrollview
            self.scrollRectToVisible(CGRect(x:0, y:childStartPoint.y,width: 1,height: self.frame.height), animated: animated)
        }
    }

    // Bonus: Scroll to top
    func scrollToTop(animated: Bool) {
        let topOffset = CGPoint(x: 0, y: -contentInset.top)
        setContentOffset(topOffset, animated: animated)
    }

    // Bonus: Scroll to bottom
    func scrollToBottom() {
        let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
        if(bottomOffset.y > 0) {
            setContentOffset(bottomOffset, animated: true)
        }
    }

}
Usman Nisar
  • 3,031
  • 33
  • 41
1

For me, the thing was the navigation bar which overlapped the small portion of the scrollView content. So I've made 2 things:

  • Size Inspector - Scroll View - Content Insets --> Change from Automatic to Never.
  • Size Inspector - Constraints- "Align Top to" (Top Alignment Constraints)- Second item --> Change from Superview.Top to Safe Area.Top and the value(constant field) set to 0

Content insets - Never Align ScrolView.Top to Safe Area.Top

Vitya Shurapov
  • 2,200
  • 2
  • 27
  • 32
1

For me scrollRectToVisible() didn't work (see here), so I used setContentOffset() and calculated it myself, based on AMAN77's answer:

extension UIScrollView {

    func scrollToView(view:UIView, animated: Bool) {
        if let superview = view.superview {
            let child = superview.convert(view.frame, to: self)
            let visible = CGRect(origin: contentOffset, size: visibleSize)
            let newOffsetY = child.minY < visible.minY ? child.minY : child.maxY > visible.maxY ? child.maxY - visible.height : nil
            if let y = newOffsetY {
                setContentOffset(CGPoint(x:0, y: y), animated: animated)
            }
        }
    }

}

It is for a horizontal scroll view, but the same idea can be applied vertically too.

Haim
  • 530
  • 4
  • 11
  • Can you show us where or how you defined `visibleSize` (needs iOS 12*) and I think you would say vertical scrollView not horizontal because of in the above code you changing `y` axis. – Coder ACJHP Jun 08 '22 at 08:59
  • Please update your code by adding this line for iOS 11; `let visibleSize = CGSize( width: self.frame.width, height: self.frame.size.height - (self.contentInset.top + self.adjustedContentInset.top) - (self.contentInset.bottom + self.adjustedContentInset.bottom) )` This will work like charm with vertical scrollView – Coder ACJHP Jun 08 '22 at 09:07
0

For scroll to top or bottom with completion of the animation

// MARK: - UIScrollView extensions

extension UIScrollView {
    /// Animate scroll to bottom with completion
    ///
    /// - Parameters:
    ///   - duration:   TimeInterval
    ///   - completion: Completion block
    func animateScrollToBottom(withDuration duration:  TimeInterval,
                            completion:             (()->())? = nil) {

        UIView.animate(withDuration: duration, animations: { [weak self] in
            self?.setContentOffset(CGPoint.zero, animated: false)
            }, completion: { finish in
                if finish { completion?() }
        })
    }

    /// Animate scroll to top with completion
    ///
    /// - Parameters:
    ///   - duration:   TimeInterval
    ///   - completion: Completion block
    func animateScrollToBottomTop(withDuration duration:  TimeInterval,
                                  completion:             (()->())? = nil) {
        UIView.animate(withDuration: duration, animations: { [weak self] in
            guard let `self` = self else {
                return
            }
            let desiredOffset = CGPoint(x: 0, y: -self.contentInset.top)
            self.setContentOffset(desiredOffset, animated: false)

            }, completion: { finish in
                if finish { completion?() }
        })
    }
}
YanSte
  • 10,661
  • 3
  • 57
  • 53
0

It is important to point out for any of you beginners out there that you will need to link the UIScrollView from your story board into you code then use the extension ".nameoffunction"

For example:

you import your UIScrollView to your code and name it bob.

you have an extension script written like the one above by "dyson returns"

you now write in your code:

"bob.scrollToTop"

This attached the extension function "scrollToTop" to you UIScrollView in the storyboard.

Good luck and chin up!

0

You can use the following method , it works well for me

func scrollViewToTop( _ someView:UIView)
{

    let targetViewTop = someView.frame.origin.y
    //If you have a complicated hierarchy it is better to 
    // use someView superview (someView.superview?.frame.origin.y) and figure out your view origin
    let viewToTop = targetViewTop - scrollView.contentInset.top
    scrollView.setContentOffset(CGPoint(x: 0, y: viewToTop), animated: true)

}

Or you can have as an extension

 extension UIScrollView
{
       func scrollViewToTop( _ someView:UIView){
    let targetViewTop = someView.frame.origin.y
    let viewToTop = targetViewTop - self.contentInset.top
    self.setContentOffset(CGPoint(x: 0, y: viewToTop), animated: true)

   }
}

here are constraints for the scroll view constraints for the scroll view

some screen shots

Screen Shots2

Atka
  • 547
  • 4
  • 7