I'm in a situation where I have two scrollable views, one horinzontally and the other vertically. Is there a way to detect if the user did a vertical or horizontal gesture, and depending on that, choose the view to interact with?
Thank you for your help.
You can use scrollViewWillBeginDragging from UIScrollViewDelegate to get the direction on which your scroll will go.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview)
if translation.y > 0 {
// swipes from top to bottom of screen -> down
} else {
// swipes from bottom to top of screen -> up
}
}
But since this method doesn't return a scroll view, you will only know which scroll was dragged and to which direction.
Please see if isDirectionalLockEnabled helps.
This helps to lock the scrolling direction.
If this property is YES and the user begins dragging in one general direction (horizontally or vertically), the scroll view disables scrolling in the other direction. If the drag direction is diagonal, then scrolling will not be locked and the user can drag in any direction until the drag completes.
Related
I have an application that uses more UIScrollViews and extensions from it like UICollectionViews and UITableViews and for a little dynamism I have created some general functions to help me with some paddings / margins.
My functions are created as extensions for UIScrollView and I want to use them just when the scroll view is only vertical.
My Question
How can I find the scrolling direction of a UIScrollView?
I have found a lot of answers of how to find the scrolling direction, but I need when the UIScrollView is initialised to find the scrolling direction of it, if it's vertical or horizontal.
Thank you for your time!
One solution I can think of in order to get the scroll direction before scrolling is to react accordingly to the type of UIScrollView in question.
So first step check if it is a UIScrollView, UICollectionView or UITableView.
In the case of a UITableView
I think a fair assumption here is that the scrolling direction will be vertical
In the case of a UIScrollView
This one could be tricky but I guess here you would have to compare the content size of the scroll view with the frame of the scrollview as Desdenova suggested and this can suggest if you can scroll horizontally and / or vertically.
In the case of a UICollectionView
This data will lie in the in the layout of the UICollectionView.
For example, I set up a basic UICollectionView in Storyboard using a FlowLayout without any data in it.
Then I add this code somewhere appropriate
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
{
if layout.scrollDirection == .horizontal {
print("horizontal")
}
else {
print("vertical")
}
}
This prints out horizontal.
For the UIScrollView you have to use the UIScrollViewDelegate scrollViewDidScroll which is helps you in UIScrollView's Scroll direction also for that you have to add some checks for the get the directions:
func scrollViewDidScroll(scrollView: UIScrollView!) {
// Update ScrollView direction
if self.lastContentOffset.y > scrollView.contentOffset.y && self.lastContentOffset.y < scrollView.contentOffset.y {
// Vertical Scroll
} else {
// Horizontal Scroll
}
}
I have a MainViewController with a bottomSheet as a childViewController on top of it.
The bottomSheet at first is presented on 1/4 of the MainViewController, with a UIPanGestureRecognizer attached to it and a UITableView as a subView of the bottomSheet pinned to the 4 edges of the view.
I can pan the bottomSheet and move it to the top of the screen. However, when it reaches the top, I need to release my finger and pan again so the UITableView receives the touch.
I need a way so when the view is at the top of the screen, it starts scrolling the tableView.
What I have tried is UIPanGestureRecognizer's changed state and setContentOffset of the UITableView, this gave me a final result, but with some issues, as it loses the system default animations
Is there a way to continue scrolling the tableView without releasing my finger?
Give the tableView it's complete height before even panning , where tableViewHeightCon is the height constraint of the tableView dragged as IBOutlet
override func viewDidLayoutSubviews() {
self.tableViewHeightCon.constant = numberOFCells*cellHeight;
}
and when you detect the view is at most top change the top constraint of the table view like this
func scrollTableAutomatic
{
UIView.animate(withDuration: 3.0) {
self.tableViewTopCon.constant -= self.tableView.frame.size.height - self.view.frame.size.height
}
}
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.
hi... really how do they implement this? there are several tutorial for Twitter profile page. but they don't handle all possibilities...
first... when you scroll top or bottom any where, top view start scrolling until the segmented control, reach at top of the page...then scroll doesn't stop and subtable start scrolling until touching down and middle of way tableview start loading other rows dynamically ... so I don't think that they set content of scrollview statically
second things how do they handle sub tables... are they containerView?
if so, then the structure would be like this
ScrollView
TopView (User Info)
Segmented Controll
scrollView(to swipe right or left changing tables)
ContainerView For TWEETS
ContainerView For TWEETS & REPLIES
ContainerView For MEDIA
ContainerView For LIKES
am I right?
so how do they handle scrolls between sub tables and Top Scroll View to implementing topview position change base on scrolling...
it's mind blowing
this is How I Handel Nested ScrollViews...
i made a childDidScroll Protocol and my child tableviews implement that and in my profile page i can receive all child didscroll event then in
childDidScroll method :
//if child scrollview going up
if(scrollView.panGestureRecognizer.translation(in: scrollView.superview).y > 0)
{
//check top scrollview if it is at bottom or top
//then disable the current scrollview
if mainScrollView.isAtBottom && scrollView.isAtTop{
scrollView.isScrollEnabled = false
}else{
//else enable scrolling for my childs
featuresVC.tableView!.isScrollEnabled = true
categoriesVC.tableView!.isScrollEnabled = true
shopsVC.tableView!.isScrollEnabled = true
}
print("up")
}
else
{
if mainScrollView.isAtTop {
scrollView.isScrollEnabled = false
mainScrollView.scrollToBottom()
}else{
featuresVC.tableView!.isScrollEnabled = true
categoriesVC.tableView!.isScrollEnabled = true
shopsVC.tableView!.isScrollEnabled = true
}
print("down")
}
but this solution has a some cons... and one of the is that first when child scrollview is at top or button, there should be two try to call my parent scrollview handle the scrolling, in first try i disable the child scrollview, and in second try parent scrollview handle the scrolling
** how can i say when you , my child, scrolling up, check if your parent is at top, then let him handle the scroll and when he touching the bottom, you can handle remain up scrolling, or tell the parent scrollview , if you are at top (user info is visible) if you or your child getting up scrolling, first you handle the scroll and when you rich at bottom(user info is not visible), let the remain scrolling on you child**
After a long long investigation that is how i achieve the twitter profile behaviour.
UnderlayScrollView
MasterScrollView
Header ViewController
Bottom ViewController
PagerTabItems [CollectionView]
UIPagerController or any other horizontal scroll (Parchment, XLPagerTabStrip).
UnderlayScrollView is responsible of controlling the scroll gesture. its contentoffset is used to adjust inner scroll contentOffsets. Contentsize of the underlaying scroll is same as the masterscroll's contentsize.
See the source code on github for more. click
I believe you are mostly right, except for the topmost scroll view.
In a recent app, I implemented something similar following this tutorial:
Basically, the trick is to have a class be the scrolling delegate of the bottom UITableViews, listen to the scrollViewDidScroll modifications, and modify the top inset of the UITableView and the TopView.
The structure I would use is like this:
Topview
ScrollView (horizontal scroll)
Segmented Control
ScrollView (horizontal, paging scroll)
UITableView
UITableView
UITableView
UITableView
You are totally right in it being mind blowing. Looks so simple.
I found a library,
https://github.com/maxep/MXSegmentedPager
Its totally works fine
I have a smaller child scrollview inside a parent scrollview. I want the child to have scrolling disabled, so that when the user drags on the screen only the parent scrollview scrolls, then once the parent reaches a certain content offset to enable scrolling on the child scrollview, with the same drag.
Currently, the child scroll is disabled, then the parent scrollview reaches the desired offset and enables scrolling on the child, but you need to take your finger off the screen and initiate a new drag to be able to scroll the now-enabled child scrollview.
Why is this? Is there any way to enable the scrollview and start scrolling it with the same drag?
Edit:
Here is the code I am currently using:
func scrollViewDidScroll (scrollView: UIScrollView) {
if scrollView == outerScroll {
if scrollView.contentOffset.y == 10 {
outerscroll.scrollEnabled = false
innerScroll.scrollEnabled = true
}
}
}