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.
Related
I have a UIScrollView with a couple of subviews, one of them is a UIStackView that can be hidden/displayed when a button is pressed.
When the UIStackView is hidden I want to disable scrolling on the UIScrollView since the content will never have enough height that justifies scrolling being enabled. To achieve that, I'm using:
isScrollEnabled.toggle()
Problem is, when doing this, the whole view gets dragged up, as if I was scrolling down, causing certain elements to be hidden behind the navigationBar and the statusBar.
I tried to fix that using the following piece of code:
if let navigationBarHeight = vcDelegate?.navigationController?.navigationBar.frame.height {
if let statusBarHeight = window?.windowScene?.statusBarManager?.statusBarFrame.height {
let totalHeight = navigationBarHeight + statusBarHeight
setContentOffset(.init(x: 0, y: -totalHeight), animated: true)
}
}
And that does actually work correctly when the contentOffset.y is >= a certain value that I can't precisely specify (because, for some reason, printing the value of contentOffset.y only shows a value that is != 0 when I scroll all the way to the bottom of the view.)
When the contentOffset.y is < than that mystery value, disabling the scroll will cause the following behavior:
The view will be dragged up
The view will be dragged down (because I call the setContentOffset function)
My bottom line question is: why does the view get dragged up when I disable scrolling? Could it be a constraint issue, some "background" offset change that I should be aware of or something else?
I want to hide the view above my table view when I scroll down and show it again when scrolling up. That kind of behavior is implemented in many apps. How can I manage to do that?
Hook the height constraint of the view above the table
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let viewY = scrollview.contentOffset.y
self.topViewH.constant = (viewY == 0) ? 200 : 0
self.view.layoutIfNeeded()
}
Problem: It is not recommended to put a tableView inside a scrollView (basically tableView itself is a scrollView). Both iOS and user will get confused how/where to scroll because of scroll inside a inside.
My solution: For your usecase, you can put your view (which you want to hide on scroll down and show on scroll up) in the tableview header itself.
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.
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 ViewController that have a UIView view and a TableView table inside it. View has big height so i want to hide it when user starts to scroll table - so table is now taking the whole screen.
To do that I use this method.
func scrollViewWillBeginDragging(scrollView: UIScrollView) {
//hide view or decrease it height
}
I hide view with animation - so it takes few moments to hide. The problem is - when user start to scroll table and view is begin to become smaller - some of top cells of table are already not showing because they are in top.
I want to pause scrolling - so that it can begin only after the view height is decreased. Also - I am afraid that it can be strange for users.
You need to make the height change as per user scroll to make it efficient.
override another delegate function
func scrollViewDidScroll(_ scrollView: UIScrollView) {
myView.heightConstraint = myView.originalHeight - scrollView.contentOffset.y;
}
The contentOffset property gives you the current amount of x,y scrolled in the scrollview from the origin.
What you can do is, you can give your tableView the entire screen and make another view with whatever height you want and make it as a header view of your tableView. So now the view will scroll as your tableView scrolls. Is this what you are looking for?
[tableView setHeaderView:<your-custom-header-view>];