How to trigger an animation when UIScrollView is scrolled to maxY? SWIFT - ios

I have a scroll view and I want an animation to start when the scrollview is scrolled to its end and a little bit further (+75 px). How is that possible? I thought about an if-condition (if view.bounds.maxy >= 1075). But how can this condition or function be called when the user scrolls?

First, make sure your ViewController is a subclass of UIScrollViewDelegate. Next, you want to set your scrollView's delegate to self in the ViewDidLoad(). Then, you'll want to use scrollViewDidScroll() and handle things from there.
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y >= 75.0 {
// Your animation code goes HERE... //
}
}

If you want your scroll to go further than the actual visible content, consider adding the required distance (+75px in your case) to the contentSize. This will enable the content to scroll further.
As for the animation, UIScrollViewDelegate has multiple methods that can help you. You can keep a check like:
if scrollView.contentOffset.y >= scrollView.contentSize.height {
// Animation code
}

Related

UITableView tap not working first time after constraints change

IMPORTANT: My problem is not that I'm implementing didDeelectRowAt instead of didSelectRowAt. Already checked that :)
I have a UITableView that is shown on part of the screen in a modally presented view controller. When the user is dragging it resizes to full screen and back to some defined min height. I'm doing this by implementing the following methods from the UIScrollViewDelegate:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard !scrollView.isDecelerating else { return }
let contentOffset = scrollView.contentOffset.y
if tableViewHeightConstraint.constant < view.frame.height && contentOffset > 0.0 {
tableViewHeightConstraint.constant = min(contentOffset + tableViewHeightConstraint.constant, view.frame.height)
scrollView.contentOffset.y = 0.0
return
}
if tableViewHeightConstraint.constant > minViewHeight && contentOffset < 0.0 {
tableViewHeightConstraint.constant = max(tableViewHeightConstraint.constant + contentOffset, minViewHeight)
scrollView.contentOffset.y = 0.0
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Here I have some calculations if depending the dragging end position and the velocity the end size should be full screen or `minViewHeight`
// After calculating what the end size should be I'm animating the size change
heightConstraint.constant = newConstraintHeight
UIView.animate(withDuration: TimeInterval(calculatedAnimationDuration), delay: 0.0, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Everything about the resizing and the scrolling works fine, but there is a problem that I cannot figure out why it's happening. It's the following:
When the view controller with the table view is shown for the first time with the min height and I tap on a cell it works fine.
If I drag to expand the table view to full screen height and tap on a cell, again it works fine.
If I drag to expand the table view to full screen height and then drag again to return it to the min height and then tap on a cell, nothing is happening, no UIScrollViewDelegate or UITableViewDelegate method is called at all. If I tap once more on a cell everything works fine.
One thing that I noticed is that after dragging the table view back to the min height the scroll indicator does not hide. On the first tap it hides, and on the second tap the didSelectRowAt is called.
UPDATE:
Here is a test repo for the problem: https://github.com/nikmin/DragTest
Please don't mind if the dragging doesn't work perfectly, I just put something so anyone can try it out, but I think the problem is easily reproducible.
Also one more thing... If you drag from full size all the way to the bottom so the table view reaches min height and you continue dragging so the content offset is < 0 and the you release, the problem is not happening.
Drag TableView to return it to the min height and then tap on a cell, nothing is happening because:
When you drag to expand the table view to full screen, scrollView.isDecelerating is true. So the code inside scrollViewDidScroll method will run.
But when you drag TableView to return it to the min height, scrollViewDidScroll is false. So the code inside scrollViewDidScroll method won't run. It's make the first tap do nothing.
Simply remove guard !scrollView.isDecelerating else { return } from scrollViewDidScroll. You will tap cell normally after drag TableView down.
But you need change logic a little, animation will go wrong after remove above line.
Hope it can help you ;)
After trying to figure out a solution to this without result, we (me and a UX designer) decided to change the behaviour a bit.
So in the real scenario in the app I'm implementing this in, the table view is inside another view that has also a title label and some other views above the table view. We decided to add a pan gesture recognizer to this root view and disable the scrolling of the table view when the view has the min size. This way the pan gesture recognizer will take over whenever the user tries to drag anywhere inside the view (including the table view), so the expanding of the view works. And the tap in the cell still works.
When the view has the max height the table view scroll is enabled so the user can scroll. The downside of this approach is that when the user scrolls to the top of the table view and continues scrolling the view will not decrease the size. But he still has the option to drag it down by dragging any of the views above the table view. When dragging down in this way, only the size of the table view changes, and the content offset isn't, which is the root of the problem (changing both at the same time).
It looks like incorrectly set content offset. The first touch cancels incorrect position(unscroll), this is why it is not registered. It might be better if we got an access to the full code to check it, because I can't tell you where exactly the problem lies, but I guess it is in method scrollViewDidScroll.

iOS swift: scrollview does not call didscrolltotop

Evening: I have a scroll view with 3 views inside..
I have a problem with the scrollview delegates.
The didscrolltotop is never called, while the did scroll yes...
I can't understand the reason...
Any help?
Looks like it is only called under certain circumstances, the documentation talks about the scroll-to-top gesture meaning it may only work after a tap on the status bar and not basic scrolling. Also setting the scrollsToTop property to true seems to be required.
The scroll view sends this message when it finishes scrolling to the top of the content. It might call it immediately if the top of the content is already shown. For the scroll-to-top gesture (a tap on the status bar) to be effective, the scrollsToTop property of the UIScrollView must be set to YES.
You could also simply detect the top of the content using the contentOffset
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("The current Y offset is \(scrollView.contentOffset.y)")
if scrollView.contentOffset.y == 0 {
print("we are at the top")
}
}
You might also want to consider using the scrollViewDidEndDecelerating method for this as it will mean you only get one event after the scroll view has settled down.

ScrollView contentOffset reset after pushViewController

I'm experiencing a problem with ScrollView, my scrollView has 640 of width, from 0 to 320 it's a mapView, from 320 to 640 it's a tableView.
I have a segmentedButton with 2 options to switch my view from contentOffset from 0 to 320 in x.
The problem is, if I switch to contentOffset of 320 in x and press a cell of tableView, after releasing the button to push other viewController, the contentOffset of my scrollView is reseting to 0 in x. I need to keep contentOffset on the tableView, at 320 in x.
I tried many things, like playing around with viewWillDisappear and the lifecycle methods.
The closest I've been able to reach was making it going back to the tableView after pressing back. Although, I couldn't make it stay on the tableView to push the next viewController.
Any help is appreciated!
(Ps: I've search for similar questions, but the only one that could help, I couldn't understand very well, the others have similar but different problems).
Thanks
I had the same problem.
Here is the solution:
1. I'm doing content offset backup in viewWillDisappear
2. Restoring offset without animation in viewWillAppear.
UPDATE: Be sure you are not adjusting contentSize in viewWillLayoutSubviews or in methods mentioned above.
I have very similar problem with UItableView which is also UIScrollView, but vertical.
Playing with viewWillDisappear: does not work because there is a noticeable scroll made by iOS when user initiated a push of another VC.
Can't resolve this problem as well.
Saving contentOffset backup at viewWillDisappear is not the correct way of handling which make you even worse to handle if you have some views which required show/hide on viewWillAppear.
The best way is saving the contentOffset at scrollViewDidScroll
Declare the variable where you want to save offset
var previousOffsetY: CGFloat = 0.0
var offsetLimitation: CGFloat = 50.0 // is the limit where I want to play show/hide
Inside UIScrollViewDelegate which is scrollViewDidScroll, save the offset.
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
previousOffsetY = scrollView.contentOffset.y
UIView.animate(withDuration: 0.5, animations: {
if offset <= self.offsetLimitation {
// show
} else {
// hide
}
})
}
}
At viewWillAppear, control the UI you want when offset is reseted.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if previousOffsetY > offsetLimitation {
// hide
}
}
check your scrollview frame when push to a new controller, if scrollview frame changed, scrollview will reset it's contentOffset

Setting contentOffset programmatically triggers scrollViewDidScroll

I've got a a few UIScrollView on a page. You can scroll them independently or lock them together and scroll them as one. The problem occurs when they are locked.
I use UIScrollViewDelegate and scrollViewDidScroll: to track movement. I query the contentOffset of the UIScrollView which changed and then reflect change to other scroll views by setting their contentOffset property to match.
Great.... except I noticed a lot of extra calls. Programmatically changing the contentOffset of my scroll views triggers the delegate method scrollViewDidScroll: to be called. I've tried using setContentOffset:animated: instead, but I'm still getting the trigger on the delegate.
How can I modify my contentOffsets programmatically to not trigger scrollViewDidScroll:?
Implementation notes....
Each UIScrollView is part of a custom UIView which uses delegate pattern to call back to the presenting UIViewController subclass that handles coordinating the various contentOffset values.
It is possible to change the content offset of a UIScrollView without triggering the delegate callback scrollViewDidScroll:, by setting the bounds of the UIScrollView with the origin set to the desired content offset.
CGRect scrollBounds = scrollView.bounds;
scrollBounds.origin = desiredContentOffset;
scrollView.bounds = scrollBounds;
Try
id scrollDelegate = scrollView.delegate;
scrollView.delegate = nil;
scrollView.contentOffset = point;
scrollView.delegate = scrollDelegate;
Worked for me.
What about using existing properties of UIScrollView?
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
/// The content offset was changed programmatically.
/// Your code goes here.
}
}
Another approach is to add some logic in your scrollViewDidScroll delegate to determine whether or not the change in content offset was triggered programatically or by the user's touch.
Add an 'isManualScroll' boolean variable to your class.
Set its initial value to false.
In scrollViewWillBeginDragging set it to true.
In your scrollViewDidScroll check to see that is it true and only respond if it is.
In scrollViewDidEndDecelerating set it to false.
In scrollViewWillEndDragging add logic to set it to false if the velocity is 0 (as scrollViewDidEndDecelerating won't be called in this case).
Simplifying #Tark's answer, you can position the scrollview without firing scrollViewDidScroll in one line like this:
scrollView.bounds.origin = CGPoint(x:0, y:100); // whatever values you'd like
This is not a direct answer to the question, but if you are getting what appear to be spurious such messages, it can ALSO be because you are changing the bounds. I am using some Apple sample code with a "tilePages" method that removes and adds subview to a scrollview. This infrequently results in additional scrollViewDidScroll: messages called immediately, so you get into a recursion which you for sure didn't expect. In my case I got a nasty impossible to find crash.
What I ended up doing was queuing the call on the main queue:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if(scrollView == yourScrollView) {
// dispatch fixes some recursive call to scrollViewDidScroll in tilePages (related to removeFromSuperView)
// The reason can be found here: http://stackoverflow.com/questions/9418311
dispatch_async(dispatch_get_main_queue(), ^{ [self tilePages]; });
}
}

How to know if UITableView is being scrolled manually (by hand) or programmatically?

I am using a UITableView in my code and it would be nice to know if the user manually scrolled the UITableView or it was done programmatically. Is there a way to know that?
UITableView is a subclass of UIScrollView.
so you can use this
if (!tableView.isDragging && !tableView.isDecelerating)
{
// the table is *not* being scrolled
}
this works. i use it in one of my apps.
You can implement following method of UIScrollViewDelegate, to know about the scrolling of your table view:
- (void)scrollViewWillBeginDragging:(UIScrollView *)activeScrollView
Don't forget to put this on too...
#interface YourViewController : UIViewController <UIScrollViewDelegate>
Hope it helps, cheers :)
Best method I've found is to use the isTracking property rather than isDragging.
if tableView.isTracking && tableView.isDecelerating {
// Table was scrolled by user.
}
Use this to detect both fast and slow scrolling caused by user interaction:
if tableView.isDragging, tableView.isDecelerating || tableView.isTracking {
// Table view is scrolled by user, not by code
}
I checked tableView.isTracking and tableView.isDecelerating inside
scrollViewDidScroll. However, it didn't work for me if the user only slightly moved the tableview. Since a scrollView has a panGesture we can check the velocity of that gesture. If the tableView was programmatically scrolled the velocity in both x and y directions is 0.0. By checking this velocity we can determine if the user scrolled the tableView because the panGesture has a velocity.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let velocity = scrollView.panGestureRecognizer.velocity(in: tableView)
print("Velocity: \(velocity)")
if velocity.x != 0.0 || velocity.y != 0.0 {
// user scrolled the tableview
}
}
You could activate a tap recognizer, detecting all touches registered.
If the tableview scrolls, but no touchgestures are involved, it should cover it.

Resources