I've put a scroll event on scrollViewDidScroll.
But when the user scrolls,
It also works when using setContentOffSet.
I want to make it work only when the user scrolls.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (self.lastContentOffset > scrollView.contentOffset.y) {
let contentSize = scrollView.contentSize.height
let tableSize = scrollView.frame.size.height - scrollView.contentInset.top - scrollView.contentInset.bottom
let canLoadFromBottom = contentSize > tableSize
// Offset
let currentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height
let difference = maximumOffset - currentOffset
if canLoadFromBottom, difference <= -85.0{
loadMore()
}
self.lastContentOffset = scrollView.contentOffset.y
}
If you scroll in code, you receive the delegate method just once immediately after, so it is easy to raise a Bool flag before scrolling in code and detect this in the delegate method.
Related
I have a UITableView with a header that I am shrinking as I scroll down, but it is creating a gap between the header and the cells.
Current code:
func moveLogoBarUp(_ scrollView: UIScrollView) {
self.tableView.bounces = true
let scrollDiff = self.tableView.contentOffset.y - self.previousScrollOffset
let newHeight = (self.tableView.tableHeaderView?.frame.height)! - abs(scrollDiff)
if newHeight <= self.tableHeaderCollapsedHeight {
self.tableView.tableHeaderView?.frame.size.height = self.tableHeaderCollapsedHeight
} else {
self.logoBarTopConstraint.constant -= abs(scrollDiff)
self.tableView.tableHeaderView?.frame.size.height = newHeight
self.tableView.contentOffset = CGPoint(x: self.tableView.contentOffset.x, y: self.previousScrollOffset)
self.previousScrollOffset = scrollView.contentOffset.y
//self.tableView.reloadData()
}
}
Note: If I use self.tableView.reloadData() it will fix the issue, but this doesn't seem ideal as it calls the method very rapidly as it scrolls.
How do I make the cells go up as the header shrinks?
The table view should recalculate the layout for the header if you set the property again. Something like this should do it:
self.tableView.tableHeaderView = self.tableView.tableHeaderView
I made a UICollectionView with a horizontal scroll.
I want to scroll only one direction i.e right to left my cell view size is as full view. once I scrolling cell, it should not scroll left to right.
Please Try this,
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let row = scrollView.contentOffset.x / cellWidth
currentIndexShown = Int(row)
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x < cellWidth * CGFloat(currentIndexShown){
scrollView.contentOffset = CGPoint(x: cellWidth * CGFloat(currentIndexShown), y: -20)
scrollView.bounces = false
} else {
scrollView.bounces = true
}
}
I am using a collection view.
Although the directionLockEnabled can be set to YES, diagonal scrolling is still enabled.
So I found a solution somewhere:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.offset = self.collectionView.contentOffset;
}
// control scroll to only horizontal & vertical
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat deltaX = ABS(self.offset.x - self.collectionView.contentOffset.x);
CGFloat deltaY = ABS(self.offset.y - self.collectionView.contentOffset.y);
if (deltaX != 0 && deltaY != 0) {
if (deltaX >= deltaY) {
self.collectionView.contentOffset = CGPointMake(self.collectionView.contentOffset.x, self.offset.y);
}
else {
self.collectionView.contentOffset = CGPointMake(self.offset.x, self.collectionView.contentOffset.y);
}
}
}
However the side effect is when I call with x, y > 0
[self.collectionView setContentOffset:CGPointMake(x, y) animated:animated];
It doesn't scroll at all, because of the code block above.
How to deal with this?
To disable manually diagonal scrolling in Swift:
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.offset = scrollView.contentOffset
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let deltaX = abs(self.offset.x - self.collectionView.contentOffset.x)
let deltaY = abs(self.offset.y - self.collectionView.contentOffset.y)
if deltaX != 0 && deltaY != 0 {
if deltaX >= deltaY {
self.collectionView.contentOffset = CGPoint(x: self.collectionView.contentOffset.x, y: self.offset.y)
} else {
self.collectionView.contentOffset = CGPoint(x: self.offset.x, y: self.collectionView.contentOffset.y)
}
}
}
OK I figure it out...
scrollView.dragging can differentiate manual scrolling or auto scrolling...
You can do
collectionView.isScrollEnabled = false
It will disable a user from scrolling, but you will be able to scroll programmatically using
collectionView.scrollToItem(at:at:animated:)
I have a UIView with a UITableView below it:
What I would like to do is to have the view above the UITableView move up (out of the way) when the user starts scrolling in the table in order to have more space for the UITableView (and come down when you scroll down again).
I know that this is normally done with a table header view, but my problem is that my table view is inside a tab (actually it is a side-scrolling page view implemented using TTSliddingPageviewcontroller). So while I only have one top UIView there are three UITableViews.
Is it possible to accomplish this manually? My first thought is to put everything in a UIScrollView, but according to Apple's documentation one should never place a UITableView inside a UIScrollView as this leads to unpredictable behavior.
Solution for Swift (Works perfectly with bounce enabled for scroll view):
var oldContentOffset = CGPointZero
let topConstraintRange = (CGFloat(120)..<CGFloat(300))
func scrollViewDidScroll(scrollView: UIScrollView) {
let delta = scrollView.contentOffset.y - oldContentOffset.y
//we compress the top view
if delta > 0 && topConstraint.constant > topConstraintRange.start && scrollView.contentOffset.y > 0 {
topConstraint.constant -= delta
scrollView.contentOffset.y -= delta
}
//we expand the top view
if delta < 0 && topConstraint.constant < topConstraintRange.end && scrollView.contentOffset.y < 0{
topConstraint.constant -= delta
scrollView.contentOffset.y -= delta
}
oldContentOffset = scrollView.contentOffset
}
Since UITableView is a subclass of UIScrollView, your table view's delegate can receive UIScrollViewDelegate methods.
In your table view's delegate:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
static CGFloat previousOffset;
CGRect rect = self.view.frame;
rect.origin.y += previousOffset - scrollView.contentOffset.y;
previousOffset = scrollView.contentOffset.y;
self.view.frame = rect;
}
More simple and fast approach
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect rect = self.view.frame;
rect.origin.y = -scrollView.contentOffset.y;
self.view.frame = rect;
}
Swift 3 & 4:
var oldContentOffset = CGPoint.zero
let topConstraintRange = (CGFloat(0)..<CGFloat(140))
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let delta = scrollView.contentOffset.y - oldContentOffset.y
//we compress the top view
if delta > 0 && yourConstraint.constant > topConstraintRange.lowerBound && scrollView.contentOffset.y > 0 {
yourConstraint.constant -= delta
scrollView.contentOffset.y -= delta
}
//we expand the top view
if delta < 0 && yourConstraint.constant < topConstraintRange.upperBound && scrollView.contentOffset.y < 0{
yourConstraint.constant -= delta
scrollView.contentOffset.y -= delta
}
oldContentOffset = scrollView.contentOffset
}
I added some constraints to the last solution to prevent some strange behaviours in case of fast scrolling
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let delta = scrollView.contentOffset.y - oldContentOffset.y
//we compress the top view
if delta > 0 && topConstraint.constant > topConstraintRange.lowerBound && scrollView.contentOffset.y > 0 {
searchHeaderTopConstraint.constant = max(topConstraintRange.lowerBound, topConstraint.constant - delta)
scrollView.contentOffset.y -= delta
}
//we expand the top view
if delta < 0 && topConstraint.constant < topConstraintRange.upperBound && scrollView.contentOffset.y < 0 {
topConstraint.constant = min(searchHeaderTopConstraint.constant - delta, topConstraintRange.upperBound)
scrollView.contentOffset.y -= delta
}
oldContentOffset = scrollView.contentOffset
}
Someone asked for the code for my solution so I am posting it here as an answer. The credit for the idea should still go to NobodyNada.
In my UITableViewController I implement this delegate method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[[NSNotificationCenter defaultCenter] postNotificationName:#"TableViewScrolled" object:nil userInfo:scrollUserInfo];
}
scrollUserInfo is a NSDictionary where I put my UITableView to pass it with the notification (I do this in viewDidLoad so I only have to do it once):
scrollUserInfo = [NSDictionary dictionaryWithObject:self.tableView forKey:#"scrollView"];
Now, in the view controller that has the view I want to move off screen while scrolling I do this in viewDidLoad:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleScroll:) name:#"TableViewScrolled" object:nil];
And finally I have the method:
- (void)handleScroll:(NSNotification *)notification {
UIScrollView *scrollView = [notification.userInfo valueForKey:#"scrollView"];
CGFloat currentOffset = scrollView.contentOffset.y;
CGFloat height = scrollView.frame.size.height;
CGFloat distanceFromBottom = scrollView.contentSize.height - currentOffset;
if (previousOffset < currentOffset && distanceFromBottom > height) {
if (currentOffset > viewHeight)
currentOffset = viewHeight;
self.topVerticalConstraint.constant += previousOffset - currentOffset;
previousOffset = currentOffset;
}
else {
if (previousOffset > currentOffset) {
if (currentOffset < 0)
currentOffset = 0;
self.topVerticalConstraint.constant += previousOffset - currentOffset;
previousOffset = currentOffset;
}
}
}
previousOffset is an instance variable CGFloat previousOffset;.
topVerticalConstraint is a NSLayoutConstraint that is set as a IBOutlet. It goes from the top of the view to the top of its superview and the initial value is 0.
It's not perfect. For instance, if the user scrolls very aggressively up the movement of the view can get a bit jerky. The issue is worse for large views; if the view is small enough there is no problem.
To create like this animation,
lazy var redView: UIView = {
let view = UIView(frame: CGRect(x: 0, y: 0, width:
self.view.frame.width, height: 100))
view.backgroundColor = .red
return view
}()
var pageMenu: CAPSPageMenu?
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(redView)
let rect = CGRect(x: 0, y: self.redView.frame.maxY, width: self.view.bounds.size.width, height:(self.view.bounds.size.height - (self.redView.frame.maxY)))
pageMenu?.view.frame = rect
self.view.addSubview(pageMenu!.view)
}
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset.y
if(offset > 100){
self.redView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: 0)
}else{
self.redView.frame = CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: 100 - offset)
}
let rect = CGRect(x: 0, y: self.redView.frame.maxY, width: self.view.bounds.size.width, height:(self.view.bounds.size.height - (self.redView.frame.maxY)))
pageMenu?.view.frame = rect
}
you must change pageMenu.view with your collectionView/tableView
I know this post in very old. I tried above solutions but neither worked for me for tried my own, hopefully it can help you. This scenario is pretty common, as apple suggested not to use TableViewController inside any ScrollView because the compiler will confused as in whom to respond becuase it will be getting two delegate call back - one from ScrollViewDelegate and another from UITableViewDelegate.
Instead we can use ScrollViewDelegate and disable the UITableViewScrolling.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat currentOffSetY = scrollView.contentOffset.y;
CGFloat diffOffset = self.lastContentOffset - currentOffSetY;
self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, 400 + [self.tableView contentSize].height);
if (self.lastContentOffset < scrollView.contentOffset.y) {
tableView.frame = CGRectMake(tableView.frame.origin.x, tableView.frame.origin.y , tableView.frame.size.width, tableView.size.height - diffOffset);
}
if (self.lastContentOffset > scrollView.contentOffset.y) {
tableView.frame = CGRectMake(tableView.frame.origin.x, tableViewframe.origin.y, tableViewframe.size.width, tableView.frame.size.height + diffOffset);
}
self.lastContentOffset = currentOffSetY;
}
Here lastContentOffset is CGFloat defined as property
The View Heirarchy is as follows:
ViewController --> View contains ScrollView (whose delegate method is defined above) --> Contain TableView.
By the above code we are manually increasing and decreasing the height of the table view along with the content size of ScrollView.
Remember to disable the Scrolling of TableView.
You can make it like this:
Add these as your class variables:
private let NUMBER_OF_ROWS: Int = 256
private let ROW_HEIGHT: CGFloat = 75.0
private let MINIMUM_CONSTANT_VALUE: CGFloat = -150.0 /// This is the hidable view's height
#IBOutlet weak var hidableViewTopConstraint: NSLayoutConstraint! /// This is an outlet from your storyboard
private var lastContentOffset: CGFloat = 0.0
This goes on your viewDidLoad():
tableView.delegate = self
And then you add this as an extension for your view controller:
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let delta = tableView.contentOffset.y - lastContentOffset
let canScrollUp: Bool =
delta < 0 &&
hidableViewTopConstraint.constant < 0 &&
scrollView.contentOffset.y < 0
let canScrollDown: Bool =
delta > 0 &&
hidableViewTopConstraint.constant > MINIMUM_CONSTANT_VALUE &&
tableView.contentOffset.y > 0
if canScrollUp || canScrollDown{
hidableViewTopConstraint.constant -= delta
tableView.contentOffset.y -= delta
}
lastContentOffset = tableView.contentOffset.y
}
}
But note that the scroll mechanism will only work when the TableView is scrolled. The top view wont collapse or expand if you scroll it.
I made an example project you can check, it is called iOSFixedHeaderList
Is it possible to add a snap-to position in a UITableView or UIScrollView? What I mean is not auto scroll to a position if I press a button or call some method to do it, I mean is if I scroll my scroll view or tableview around a specific point, say 0, 30, it will auto-snap to it and stay there? So if my scroll view or table view scrolls and then the user lets go inbetween 0, 25 or 0, 35, it will auto "snap" and scroll there? I can imagine maybe putting in an if-statement to test if the position falls in that area in either the scrollViewDidEndDragging:WillDecelerate: or scrollViewWillBeginDecelerating: methods of UIScrollView but I'm unsure how to implement this in the case of a UITableView. Any guidance would be much appreciated.
Like Pavan stated, scrollViewWillEndDragging: withVelocity: targetContentOffset: is the method you should use. It works with table views and scroll views. The code below should work for you if you are using a table view or vertically scrolling scroll view. 44.0 is the height of the table cells in the sample code so you will need to adjust that value to the height of your cells. If used for a horizontally scrolling scroll view, swap the y's for x's and change the 44.0 to the width of the individual divisions in the scroll view.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
// Determine which table cell the scrolling will stop on.
CGFloat cellHeight = 44.0f;
NSInteger cellIndex = floor(targetContentOffset->y / cellHeight);
// Round to the next cell if the scrolling will stop over halfway to the next cell.
if ((targetContentOffset->y - (floor(targetContentOffset->y / cellHeight) * cellHeight)) > cellHeight) {
cellIndex++;
}
// Adjust stopping point to exact beginning of cell.
targetContentOffset->y = cellIndex * cellHeight;
}
I urge you to use the
scrollViewWillEndDragging: withVelocity: targetContentOffset: method instead which is meant to be used for exactly your purpose. to set the target content offset to your desired position.
I also suggest you look at the duplicate questions already posted on SO.
Take a look at these posts
If you really must do this manually, here is a Swift3 version. However, it's highly recommended to just turn on paging for the UITableView and this is handled for you already.
let cellHeight = 139
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.pointee.y = round(targetContentOffset.pointee.y / cellHeight) * cellHeight
}
// Or simply
self.tableView.isPagingEnabled = true
In Swift 2.0 when the table has a content inset and simplifying things, ninefifteen's great answer becomes:
func scrollViewWillEndDragging(scrollView: UIScrollView,
withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>)
{
let cellHeight = 44
let y = targetContentOffset.memory.y + scrollView.contentInset.top + cellHeight / 2
var cellIndex = floor(y / cellHeight)
targetContentOffset.memory.y = cellIndex * cellHeight - scrollView.contentInset.top;
}
By just adding cellHeight / 2, ninefifteen's if-statement to increment the index is no longer needed.
Here's the Swift 2.0 equivalent
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let cellWidth = CGRectGetWidth(frame) / 7 // 7 days
var index = round(targetContentOffset.memory.x / cellWidth)
targetContentOffset.memory.x = index * cellWidth
}
And this complicated rounding isn't necessary at all as long as you use round instead of floor
This code lets you snap to a cell, even when cells have a variable (or unknown) height, and will snap to the next row if you'll scroll over the bottom half of the row, making it more "natural".
Original Swift 4.2 code: (for convenience, this is the actual code I developed and tested)
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard var scrollingToIP = table.indexPathForRow(at: CGPoint(x: 0, y: targetContentOffset.pointee.y)) else {
return
}
var scrollingToRect = table.rectForRow(at: scrollingToIP)
let roundingRow = Int(((targetContentOffset.pointee.y - scrollingToRect.origin.y) / scrollingToRect.size.height).rounded())
scrollingToIP.row += roundingRow
scrollingToRect = table.rectForRow(at: scrollingToIP)
targetContentOffset.pointee.y = scrollingToRect.origin.y
}
(translated) Objective-C code: (since this question is tagged objective-c)
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
NSIndexPath *scrollingToIP = [self.tableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
if (scrollingToIP == nil)
return;
CGRect scrollingToRect = [table rectForRowAtIndexPath:scrollingToIP];
NSInteger roundingRow = (NSInteger)(round(targetContentOffset->y - scrollingToRect.origin.y) / scrollingToRect.size.height));
scrollingToIP.row += roundingRow;
scrollingToRect = [table rectForRowAtIndexPath:scrollingToIP];
targetContentOffset->y = scrollingToRect.origin.y;
}
I believe that if you use delegate method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
NSLog(#"%f",scrollView.contentOffset.y);
if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370)
{
NSLog(#"setContentOffset");
[scrollView setContentOffset:CGPointMake(0, 350) animated:YES];
}
}
Play around with it until you get the desired behaviour.
Maybe calculate the next top edge of the next tableViewCell/UIView and stop at the top of the nearest one at the slow down.
The reason for this: if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370) is that the scroll view calls scrollViewDidScroll in jumps at fast speeds so I give a between if.
You can also know the speed is slowing down by:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
NSLog(#"%f",scrollView.contentOffset.y);
int scrollSpeed = abs(scrollView.contentOffset.y - previousScrollViewYOffset);
previousTableViewYOffset = scrollView.contentOffset.y;
if (scrollView.contentOffset.y > 350 && scrollView.contentOffset.y < 370 && scrollSpeed < minSpeedToStop)
{
NSLog(#"setContentOffset");
[scrollView setContentOffset:CGPointMake(0, 350) animated:YES];
}
}
Updated for Swift 5
As a bonus, it works for whatever the height is of the cell it was going to land on
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let indexPath = self.tableView.indexPathForRow(at: CGPoint(x: self.tableView.frame.midX, y: targetContentOffset.pointee.y)) else {
return
}
var cellHeight = tableView(self.tableView, heightForRowAt: indexPath)
let cellIndex = floor(targetContentOffset.pointee.y / cellHeight)
if targetContentOffset.pointee.y - (floor(targetContentOffset.pointee.y / cellHeight) * cellHeight) > cellHeight {
cellHeight += 1
}
targetContentOffset.pointee.y = cellIndex * cellHeight
}