I want a horizontal scrollable collection view to scroll in both side(left and right), now it has continuous scrolling only in right direction, how to implement in left side ?
let itemcount = array?.count ?? 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == DemoCollectionView {
let offSet = scrollView.contentOffset.x
let width = scrollView.frame.width
let horizontalCenter = width / 2
let currentPage = Int(offSet + horizontalCenter) / Int(width)
if currentPage >= itemCount / 2 {
itemCount += array?.count ?? 0
DemoCollectionView.reloadData()
}
}
}
You can do it using "scrollToItemAtIndexPath" method. Swipe direction you can get from scrollview delegate methods.
Scroll right to left
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: dataArray.count - 1, inSection: 0), atScrollPosition: .Right, animated: false)
Scroll left to right
collectionView?.scrollToItemAtIndexPath(NSIndexPath(forItem: 0, inSection: 0), atScrollPosition: .left, animated: false)
You can also apply a transformation on collection view to get similar.
I made an UIScrollView in a XIB for my onboarding. The UIScrollView has 3 onboarding views. Long story short:
This works perfect. However I want the top left and right buttons (Overslaan - Volgende) to animate up / off the screen when the third/last page is on screen. My UIScrollView starts behaving weird when I animate the buttons off:
This is the code im using:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.frame.width)
pageControl.currentPage = pageIndex
if stepViews[pageIndex] is OnboardingLoginView {
moveControlConstraintsOffScreen()
} else {
moveControlConstraintsOnScreen()
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
}, completion: nil)
}
I debugged the code and it turns out that setting a new constant for the constraints causes the issue, regardless of the animation block. How do I make the buttons move up/off the screen without my scrollView behaving weird?
It looks like triggering a layout pass is interfering with your scroll view positioning. You could implement func scrollViewDidScroll(_ scrollView: UIScrollView) and try to update the button view on a per-frame basis, without changing Auto Layout constraints. I usually use the transform property for frame changes outside of Auto Layout, since any changes to frame or bounds are overwritten during the next layout pass.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard mustBeOnLastPage else {
buttonOne.tranform = .identity
buttonTwo.tranform = .identity
return
}
let offset = scrollView.contentOffset.x
buttonOne.tranform = .init(translationX: 0, y: offset)
buttonTwo.tranform = .init(translationX: 0, y: offset)
}
Old answer
This interpretation is a bit of tangent of what you're asking for.
Since it looks like you're using the scroll view in a paging context, I would approach this problem by using UIPageViewController. Since UIPageViewController uses UIScrollView internally, you can observe the contentOffset of the last view in the scroll view to determine how far along the page has scrolled.
Yes, this involves looking inside the view hierarchy, but Apple hasn't changed it for half a decade so you should be safe. Coincidentally, I actually implemented this approach last week and it works like a charm.
If you're interested, I can further expand on this topic. The post that pointed me in the right direction can be found here.
Here is what i do
I can't post image,you can look at this http://i.imgur.com/U7FHoMu.gif
and this is the code
var centerYConstraint: Constraint!
func setupConstraint() {
fadeView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
centerYConstraint = make.centerY.equalToSuperview().constraint
make.size.equalTo(CGSize(width: 50, height: 50))
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint,targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.view.frame.width)
if pageIndex != 1 {
centerYConstraint.update(offset: self.view.frame.height)
} else {
centerYConstraint.update(offset: 0)
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Based on #CloakedEddy's answer I made 2 changes:
1: It seems layoutSubviews is responsible for the weird behaviour. To fix this I prevent the scrollView from calling layoutSubviews all the time:
override func layoutSubviews() {
super.layoutSubviews()
if !didLayoutSubviews {
for index in 0..<stepViews.count {
let page: UIView = stepViews[index]
let xPosition = scrollView.frame.width * CGFloat(index)
page.frame = CGRect(x: xPosition, y: 0, width: scrollView.bounds.width, height: scrollView.frame.height)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(index + 1)
}
didLayoutSubviews = true
}
}
2: If you want to update your views for device orientations:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
onboardingView.didLayoutSubviews = false
onboardingView.setNeedsLayout()
}
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)
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)
}
}
}
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];
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.
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()
})
}
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)
}
}
}
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)
}
}
}
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
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.
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?() }
})
}
}
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!
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
some screen shots
Screen Shots2
When the user lets go of scrolling in a UICollectionView, I want it to scroll to a relevant point. In this example, I'll use CGPoint(x: 0, y: 100).
I've previously tried doing this:
func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
self.collectionView.setContentOffset(CGPoint(x: 0, y: 100), animated: true)
}
But that gives an animation that starts with a CurveEaseIn. If the user were to let go as they were dragging, this would give an odd appearance as the dragging suddenly halts as they lift their finger, then starts moving again.
So I tried doing this with my own animation instead:
UIView.animateWithDuration(
0.3,
delay: 0,
options: UIViewAnimationOptions.CurveEaseOut,
animations: {
() -> Void in
self.collectionView.setContentOffset(CGPoint(x: 0, y: 100), animated: false)
}, completion: nil)
The animation take place just like I'd want, but the cells which would be off screen at the end of the animation disappear immediately. The same thing happens if I replace setContentOffset() with contentOffset =.
So then somebody suggest that instead, I use the scrollViewWillEndDragging:withVelocity:targetContentOffset: method. So I did:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.memory = CGPoint(x: 0, y: 100)
}
What happens now is the animation occurs with the given velocity, so if the user lifts their finger, then it slowly makes its way to the desired location, with 0 velocity.
I'm attempting to find a good way to restrict user scrolling around certain sections within my UITableView. For instance, when section 3 is selected (by my logic) I would like to have it locked at the top of the screen so you cannot scroll away from it until it is unlocked, but the important part is I still want to keep the responsive feel of the UIScrollView (ie. the bounce interactivity).
I attempted to recreate the logic on my own as seen below (which works pretty well, but doesn't have the same bounce feel I am looking for):
override func scrollViewDidScroll(scrollView: UIScrollView) {
let sectionRect = tableView.rectForSection(activeHeaderSection)
let topY = sectionRect.origin.y
let bottomY = topY + sectionRect.height
let frameSize = self.tableView.frame.size.height - self.navigationController!.navigationBar.frame.size.height
let translation = scrollView.panGestureRecognizer.translationInView(scrollView)
if (scrollView.contentOffset.y + frameSize > bottomY) && (frameSize < sectionRect.size.height) && (translation.y < 0) {
UIView.animateLinear(0.7, initialSpringVelocity: 0, animations: { self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: self.checkTuple[self.activeHeaderSection].pickChecks.count-1, inSection: self.activeHeaderSection), atScrollPosition: .Bottom, animated: false) }, completion: nil)
} else if (frameSize > sectionRect.size.height) || (scrollView.contentOffset.y < topY) {
UIView.animateLinear(0.7, initialSpringVelocity: 0, animations: { self.tableView.scrollToRowAtIndexPath(NSIndexPath(forRow: 0, inSection: self.activeHeaderSection), atScrollPosition: .Top, animated: false) }, completion: nil)
}
}
So I did some searching around here and some question pointed towards restricting the scrollView's contentView height and position (ie. something like this) :
override func scrollViewDidScroll(scrollView: UIScrollView) {
var activeHeaderSection = 4
let sectionRect = tableView.rectForSection(activeHeaderSection)
tableView.contentOffset.y = sectionRect.origin.y // Set once when locked
tableView.contentSize.height = sectionRect.size.height
tableView.clipsToBounds = false
}
But this also isn't really giving me the behaviour I want as it always sticks to the top of my UITableView (with the correct height) but does not scroll down to the proper section. I'd prefer to use something simple like the second method if possible, I'm not sure if there is an even easier method for accomplishing something like this so I thought I'd ask here.
I think this
tableView.contentOffset.y = sectionRect.origin.y
tableView.contentSize.height = sectionRect.size.height
should be:
tableView.contentInset.top = -sectionRect.origin.y
tableView.contentSize.height = sectionRect.origin.y + sectionRect.size.height