Mimic adjustedContentInset on iOS 10? - ios

I'm looking modify the size and position of a tableView's cell's subview from scrollViewDidScroll(_:) by measuring the contentOffset. The contentOffset seems to jump to a non-zero value the first time that scrollViewDidScroll(_:) is called.
For a UITableView within UINavigationController, adjustedContentInset on iOS 11 correctly allows me to calculate the relative contentOffset when scrolling the tableView.
For example, if I scroll the tableView, I can calculate the precise amount that it was scrolled like this:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
var adjustedTopInset: CGFloat = 0.0
if #available(iOS 11.0, *) {
adjustedTopInset = scrollView.adjustedContentInset.top
}
let deltaY = scrollView.contentOffset.y - adjustedTopInset
}
The thing is, as adjustedContentInset is only available on iOS 11, how do you calculate this same relative scroll amount on iOS 10?
In viewDidLoad() the contentOffset of the tableView is CGPointZero, but the first time that scrollViewDidScroll(_:) is called, the contentOffset.y jumps to a value that is the height of the navigation bar and the status bar. On iOS 11 adjustedContentInset helps by providing that height.
How can I calculate this on iOS 10?

It sound as if this might be an "x-y" problem. You haven't explained what you're really trying to do, but the mention of viewDidLoad suggests that you are trying to get some measurement value in viewDidLoad so as to know later how much that measurement has changed. But you cannot measure anything in viewDidLoad; it's too early. Postpone the measurement until the first call to viewDidAppear and you will then have a correct starting value.

Related

How do I find UITableview scroll difference - swift

How to find the scroll difference of a UITableView.
I need to find the difference of the Tableview content scroll from the tableview top position in swift
I tried the below code,
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = tableview.frame.origin.y
let maximumOffset = (contentOffset - scrollView.contentOffset.y)
let dif = maximumOffset
}
Contents offset of tableview, itself describe as difference between top of content to current scroll position.
Try this and see:
print(tableView.contentOffset)
For more, see Apple Document - contentOffset
contentOffset : The point at which the origin of the content view is offset from the origin of the scroll view.
Also look at this refernce:
Understanding of Content Offset
A UITableView is a subclass of UIScrollView, so it has a contentOffset property.
When you're learning about a class, remember to check it's parent class. Sometimes the properties/methods you're looking for are in the parent. (I'm as guilty of forgetting to check the interface of parent classes as anybody.)

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.

scrolling/resizing UITableView

I'll get right to the point.
I have a UIViewController that has two subviews in it. The top one (let's call it HeaderView from now one) is a custom UIView and the bottom one is a UITableView.
I have set them up in InterfaceBuilder so that the HeaderView has 0 margin from the left, top and right, plus it has a fixed height.
The UITableView is directly underneath with 0 margin from all sides.
My goal is to achieve a behaviour such that when I start scrolling the UITableView's content the HeaderView will start shrinking and the UITableView becomes higher without scrolling. This should go on until the HeaderView has reached a minimum height. After that the UITableView should start scrolling as normal. When scrolling down the effect should be reversed.
I have initially started this out using a UIScrollView instead of the UITableView and I have achieved the desired result. Here is how:
connect the UIScrollView to the outlet
#IBOutlet weak var scrollView: UIScrollView!
set the UIScrollViewDelegate in the controller's viewDidLoad() method
self.scrollView.delegate = self
and declared the UIViewController to conform to the protocol
intercept when the UIScrollView scrolls:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.adjustScrolling(offset: scrollView.contentOffset.y, scrollView: scrollView)
}
in my adjustScrolling(offset:scrollView:) method the "magic" happens
Now let's look at what happens in this method.
private func adjustScrolling(offset: CGFloat, scrollView: UIScrollView) {
// bind value between 0 and max header scroll
let actualOffset: CGFloat = offset < 0 ? 0 : (offset >= self.maxHeaderScroll ? self.maxHeaderScroll : offset)
// avoid useless calculations
if (actualOffset == self.currentOffset) {
return
}
/**
* Apply the vertical scrolling to the header
*/
// Translate the header up to give more space to the scrollView
let headerTransform = CATransform3DTranslate(CATransform3DIdentity, 0, -(actualOffset), 0)
self.header.layer.transform = headerTransform
// Adjust header's subviews to new size
self.header.didScrollBy(actualOffset)
/**
* Apply the corrected vertical scrolling to the scrollView
*/
// Resize the scrollView to fill all empty space
let newScrollViewY = self.header.frame.origin.y + self.header.frame.height
scrollView.frame = CGRect(
x: 0,
y: newScrollViewY,
width: scrollView.frame.width,
height: scrollView.frame.height + (scrollView.frame.origin.y - newScrollViewY)
)
// Translate the scrollView's content view down to contrast scrolling
let scrollTransform = CATransform3DTranslate(CATransform3DIdentity, 0, (actualOffset), 0)
scrollView.subviews[0].layer.transform = scrollTransform
// Set bottom inset to show content hidden by translation
scrollView.contentInset = UIEdgeInsets(
top: 0,
left: 0,
bottom: actualOffset,
right: 0
)
self.currentOffset = actualOffset
}
If I haven't forgotten anything this should be enough to achieve the desired effect. Let me break it down:
I calculate the actualOffset binding it between 0 and self.MaxHeaderScroll which is just 67 (I think, it's calculated dynamically but this doesn't really matter)
If I see that the actualOffset hasn't changed since the last time this function was called I don't bother to aplly any changes. This avoids some useless calculations.
I apply the scrolling to the header by translating it up with a CATransform3DTranslate on just the y axis by negative actualOffset.
I call self.header.didScrollBy(actualOffset) so that the HeaderView can apply some visual changes internally. This doesn't concearn the question though.
I resize the scrollView so that it keeps 0 margin from top and bottom now that the HeaderView is higher up.
I translate down the scrollView's content by the same actualOffset amount to contrast the scrolling. This piece is essential to the correct visual effect that I want to achieve. If I didn't do this, the scrollView would still resize correctly but the content would start scrolling right away, which I don't want. It should only start scrolling once the HeaderView reaches it's minimum height.
I now set a bottom inset in the scrollView so that I am able to scroll it all the way to the end. Without this, the last part of the scrollView would be cut off since the scrollView itself would think it reached the end of it's content.
Lastly I store the actualOffset for later comparison
As I said, this works fine. The problem arises when I switch from a UIScrollView to a UITableView. I assumed it would work since UITableView inherits from UIScrollView.
The only piece of code that doesn't work is the number 6. I don't really know what is going wrong so I will just list everything I have found out and/or noticed. Hopefully someone will be able to help me out.
in the case of the UIScrollView, in point 6, the scrollView.subviews[0] refers to a view that holds all the content inside it. When I change to UITableView this subview seems to be of the type UITableViewWrapperView which I could not find any documentation about, nor does XCode recognize it as a valid class. This is already frustrating.
if in point 6 I also give some translation on the x axis (let's say of 50) I can see an initial very quick translation that is immediately brought back to 0. This only happens when the UITableView starts scrolling, it doesn't go on while scrolling.
I have tried changing the frame of the subview in point 6 to achieve the desired result. Although the scrolling is correct, the top cells start disappearing as I scroll the UITableView. I thin this is because I am using dequeueReusableCell(withIdentifier:for:) to instatiate the cells and the UITableView thinks that the top cells aren't visible when they actually are. I wasn't able to work around this problem.
I have tried setting the self.tableView.tableHeaderView to a UIView of the actualOffset height to contrast scrolling but this gave a weird effect where the cells would not scroll correctly and when the UITableView was brought back to the initial position, there would be a gap on top. No clue about this either.
I know there's a lot here so please don't hesitate asking for more details. Thank you in advance.
I made something like this recently, so heres how I achieved it:
Make a UIView with a height constraint constant and link this to your view/VC, have you UITableview constrained to the VC's view full screen behind the UIView.
Now set your UITableViews contentInset top to the starting height of your 'headerView' now, in the scrollViewDidScroll you adjust the constant until the height of the header is at its minimum.
Here is a demo
If you just run it, the blue area is your 'header' and the colored rows are just any cell. You can autolayout whatever you want in the blue area and it should auto size and everything

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

UITableView dragging distance with UIRefreshControl

I'm having some trouble on implementing a UIRefreshControl on a UITableView.
Everything is working fine except the fact that I have to scroll something like 80% of the screen for the UIRefreshControl to get triggered. Sometimes I'm not even able to trigger it since there is a tab bar on the bottom of the screen, which cancels the scrolling movement when the finger reaches it.
I've looked at other apps, namely Apple's 'Mail', where the UIRefreshControl is triggered after scrolling only 30% of the screen.
What am I missing? Really need help on this one!
Thanks in advance
I had a similar problem and it's quite possible that's the same cause for you.
For me happens that I hided the scroll indicator making me unable to see the obvious cause of the problem: the UIScrollView's height is much greater than its superView...
Double check your UIScrollView's height because the "dragging distance" it's just a percentage of that height. Same goes for UITableView too, since it's a child class of UIScrollView.
EDIT:
Seems that this isn't the only way to reproduce this problem, since the required drag distance to trigger the refresher is calculated in a buggy way. Refer to this question for more info.
But in general it will happen if your UIScrollView's height is different than his parent container (e.g the screen itself).
You probably need to not use UIRefreshControl and just utilize scrollViewDidScroll (or tableViewDidScroll if a tableView) on handle your refresh accordingly since UIRefreshControl can't be modified.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height)
{
// Refresh from here
}
}
After some extensive testing and playing around with UIKit I have come to a conclusion.
TL;DR
UIRefreshControl isn't smart enough. To fix add this code in viewDidAppear or viewDidLayoutSubviews
let refreshControl = scrollView.refreshControl
scrollView.refreshControl = nil
scrollView.refreshControl = refreshControl
Test setup
Single UIViewController in a Storyboard with a UIView as its view which in turn has a UIScrollView as only subview which in turn has a single UIView as subview with top,right,bottom,left,width,height constraints equal to superview. The viewController has freeform size with 1200p height.
In the subclass of the UIViewController a UIRefreshControl is added by setting the UIScrollView#refreshControl to a new UIRefreshControl inside viewDidLoad.
When running the application in an iPhone X simulator and dragging the scrollView to perform a "Pull to refresh" one must drag considerably longer to make the refreshControl animating and send its notification it was pulled down.
The problem
One of my hypotheses was that the UIRefreshControl gets its dragging distance set once it is added to the scrollView and since AutoLayout hasen't updated the root view's subviews at viewDidLoad the scrollView has a height of 1180.0p instead of the correct 768.0p thus the refreshControl's dragging distance would be calculated for the height 1180.0p instead of 768.0p and thus become much longer than expected when running the app.
The solution
By updating the scrollView's refreshControl after the correct size for the scrollView has been set the correct dragging distance is calculated.
This update has to occur in a function where the correct size of the scrollView has been calculated, for example viewDidAppear and viewDidLayoutSubviews.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let refreshControl = scrollView.refreshControl
scrollView.refreshControl = nil
scrollView.refreshControl = refreshControl
}

Resources