Making multiple scrollviews respond as one takes out the scrolling behaviour - ios

I have a view, with four scrollviews inside of it. What I'd like to do is make all these scrollviews respond when one of them is scrolled. I tried to do that through this code:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.hpcScrollView.setContentOffset(scrollView.contentOffset, animated: true)
self.parScrollView.setContentOffset(scrollView.contentOffset, animated: true)
self.scoreScrollView.setContentOffset(scrollView.contentOffset, animated: true)
self.holeScrollView.setContentOffset(scrollView.contentOffset, animated: true)
}
however, when I apply this, the scrolling "behavior" is taken away. It does still scroll, but there's no bounce, and when I take my finger off the scrollview, it immediately stops scrolling (see gif below)
Is there a way to make this feel like normal scrolling?

Do not apply the contentOffset to the scrollView which is currently scrolling. Changing the contentOffset on a currently scrolling view would cause it to stop scrolling and immediately jump to the offset.
let scrollableViews = [hpcScrollView, parScrollView, scoreScrollView, holeScrollView]
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollableViews.forEach { if $0 != scrollView { $0.setContentOffset(scrollView.contentOffset, animated: true) } }
}
EDIT:
The scrollViewDidScroll is getting called whenever we're setting the content offset, which causes the weird scrolling behaviors shown in the gif.
try this instead
let scrollableViews = [hpcScrollView, parScrollView, scoreScrollView, holeScrollView]
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollableViews.forEach { if $0 != scrollView {
let scrollBounds = $0.bounds
scrollBounds.origin = scrollView.contentOffset
$0.bounds = scrollBounds
} }
}

The problem is that you reset the originating scroll view, which makes it stop. You need to keep track of the current scroll view the user is interacting with and only update the other scroll views. Otherwise they will all generate scroll events and end up updating each other.
// currentScrollView is an object var
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
currentScrollView = scrollView
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == currentScrollView {
if currentScrollView != self.hpcScrollView {
self.hpcScrollView.setContentOffset(scrollView.contentOffset, animated: false)
}
if currentScrollView != self.parScrollView {
self.parScrollView.setContentOffset(scrollView.contentOffset, animated: false)
}
if currentScrollView != self.scoreScrollView {
self.scoreScrollView.setContentOffset(scrollView.contentOffset, animated: false)
}
if currentScrollView != self.holeScrollView {
self.holeScrollView.setContentOffset(scrollView.contentOffset, animated: false)
}
}
}
Note that you must set the animated flags to false.

Related

Manually scrolling UIScrollView which is animated by UIViewPropertyAnimator

I have a UIScrollView which scrolls automatically by setting its content offset within via a UIViewPropertyAnimator. The auto-scrolling is working as expected, however I also want to be able to interrupt the animation to scroll manually.
This seems to be one of the selling points of UIViewPropertyAnimator:
...dynamically modify your animations before they finish
However it doesn't seem to play nicely with scroll views (unless I'm doing something wrong here).
For the most part, it is working. When I scroll during animation, it pauses, then resumes once deceleration has ended. However, as I scroll towards the bottom, it rubber bands as if it is already at the end of the content (even if it is nowhere near). This is not an issue while scrolling towards the top.
Having noticed this, I checked the value of scrollView.contentOffset and it seems that it is stuck at the maximum value + the rubber banding offset. I found this question/answer which seems to be indicate this could be a bug with UIViewPropertyAnimator.
My code is as follows:
private var maxYOffset: CGFloat = .zero
private var interruptedFraction: CGFloat = .zero
override func layoutSubviews() {
super.layoutSubviews()
self.maxYOffset = self.scrollView.contentSize.height - self.scrollView.frame.height
}
private func scrollToEnd() {
let maxOffset = CGPoint(x: .zero, y: self.maxYOffset)
let duration = (Double(self.script.wordCount) / Double(self.viewModel.wordsPerMinute)) * 60.0
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
self.scrollView.contentOffset = maxOffset
}
animator.startAnimation()
self.scrollAnimator = animator
}
extension UIAutoScrollView: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// A user initiated pan gesture will begin scrolling.
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
self.interruptedFraction = scrollAnimator.fractionComplete
scrollAnimator.pauseAnimation()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
switch scrollView.panGestureRecognizer.state {
case .changed:
// A user initiated pan gesture triggered scrolling.
if let scrollAnimator = self.scrollAnimator {
let fraction = (scrollView.contentOffset.y - self.maxYOffset) / self.maxYOffset
let boundedFraction = min(max(.zero, fraction), 1)
scrollAnimator.fractionComplete = boundedFraction + self.interruptedFraction
}
default:
break
}
}
}
Is there anywhere obvious I'm going wrong here? Or any workarounds I can employ to make the scroll view stop rubber banding on scroll downwards?
You can add tap Gesture Recognizer and call this function,
extension UIScrollView {
func stopDecelerating() {
let contentOffset = self.contentOffset
self.setContentOffset(contentOffset, animated: false)
}
}

Get type of UIScrollView inside UIScrollViewDelegate methods

I have a UIViewController that has UITableView and UICollectionView. I want to do certain tasks when UICollectionView is scrolled.
I've extended UIScrollViewDelegate and wrote my code in
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView)
But this method is called when both UITableView and UICollectionView are scrolled. How do I differentiate between the two views here? I tried
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if let cv = scrollView as? UICollectionView {
}
}
But it's not working. I tried po scrollView and the output is <uninitialized>.
// Say tv and cv are outlets to table View and Collection View
Objective c
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.tv) {
self.tv.contentOffset = self.tv.contentOffset;
} else if (scrollView == self.cv) {
self.cv.contentOffset = self.cv.contentOffset;
}
}
Swift
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if let scrollView == tv {
//do whatever you need with tableView
}
if let scrollView == cv {
//do whatever you need with collectionView
}
}
It was a mistake on my end but for others here I'll post the answer.
I needed to execute my code when slider has finished dragging/scrolling. To achieve that I had
func scrollViewDidScroll(_ scrollView: UIScrollView) {
NSObject.cancelPreviousPerformRequests(withTarget: self)
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
}
Problem was in line
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: nil, afterDelay: 0.3)
Instead of passing nil, I wrote
perform(#selector(UIScrollViewDelegate.scrollViewDidEndScrollingAnimation), with: scrollView afterDelay: 0.3)
and that resolved it. I wasn't getting scrollView <uninitialized> anymore and my
if let cv = scrollView as? UICollectionView {
}
worked fine.

How can I detect when iOS ScrollView is at the very top?

How can I detect when the user scrolled to the very top of my collectionView?
I tried to use this code:
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if collectionView.contentOffset.y <= collectionView.contentSize.height - collectionView.frame.size.height {
print("top")
}
}
But it prints "top" even if I didn't get to the top.
scrollViewDidEndDragging means the user's finger lifted, but because it has physics to simulate inertia, the view may continue to move. There is another delegate method for exactly what you are looking for: scrollViewDidScrollToTop which as the name implies fires when you get to the top of the scrollview.
EDIT:
you can also try:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y <= 0 {
print("top!")
}
}
EDIT third time's the charm? This one only fires once, after the scroll view has stopped moving, if you are at the top.
override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == 0 {
print("top!")
}
}
You can try
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == 0 {
// top
}
}

Easiest way to disable back scrolling in scrollview

I want to stop backward scrolling on ScrollView after user scrolls to the next page. How can I do that.
I tried the following two codes, but the first one does not have any effect
self.scrollView.contentOffset = CGPointMake(self.view.frame.width,0)
and the second only disables the forward scrolling.
self.scrollView.contentSize = CGSizeMake( 2 * scrollWidth, scrollHeight);
To disable scrolling in one direction you implement the UIScrollViewDelegate method scrollViewDidScroll and put your logic there. For instance this TableViewController can only ever scroll down, because if the user tries to scroll up, we just overwrite the contentOffset, effectively undoing their scroll before they see it.
class ViewController: UITableViewController {
var lastScrollPosition = CGPoint.zero
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.y > lastScrollPosition.y else {
scrollView.setContentOffset(lastScrollPosition, animated: false)
return
}
lastScrollPosition = scrollView.contentOffset
}
}
If your cell is equal in size to your screen, you can apply the following option, which is very smooth:
var lastScrollPosition = CGPoint.zero
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x == lastScrollPosition.x + UIScreen.main.bounds.width {
lastScrollPosition.x += UIScreen.main.bounds.width
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard scrollView.contentOffset.x > lastScrollPosition.x else {
scrollView.setContentOffset(lastScrollPosition, animated: false)
return
}
}

Detect when Scrollview Scrolling not tableview scrolling

I have a scrollview inside tableview (tableview's header) and I want to call this action when scroll finish:
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
let pageNumber = round(bookScrollView.contentOffset.x / bookScrollView.frame.size.width)
pageControl.currentPage = Int(pageNumber)
print("scroll")
}
when I scroll bookScrollView nothing happen, but when table view scroll this function run.
How I detect when bookScrollView scroll and run function?
thanks
Here, you can define, scrollOfTable, or you can also define TAG property. identify and only scroll when scrollview comes in, not tableview.
You also need to check, if your scrollView is setting delegate
self.vsScrollView.delegate = self
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
if scrollView == scrollOfTable {
}
else if scrollView == scrollOfScrollView {
}
else {
//something else
}
}

Resources