ScrollView contentOffset reset after pushViewController - ios

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

Related

Mimic adjustedContentInset on iOS 10?

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.

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.

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

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
}

Swift LayoutSubviews moving all subviews every user interaction

I apologize if this question has been already addressed, but I cannot find it.
I thought it would be simple enough to animate a UIImageView from offscreen onto the screen at certain points in my program. So I created the UIImageView and coded it up, but am getting a lot of strange behaviour. I have narrowed it down to the fact that layoutSubviews is getting called every user interaction (every button press) and it is resetting the size and location of all my items. I am using auto layout for every screen item save this one, so that is fine for the things I want auto layed out. But this one item is constantly being moved back to it's starting location. I proved it this way:
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad")
let screenWidth = UIScreen.mainScreen().bounds.width
let screenHeight = UIScreen.mainScreen().bounds.height
print(image.center)
image.center = CGPointMake(screenWidth / 2, image.bounds.height / -2)
print(image.center)
}
override func viewWillLayoutSubviews() {
print("viewWillLayoutSubviews")
print(image.center)
}
override func viewDidLayoutSubviews() {
print("viewDidLayoutSubviews")
print(image.center)
}
This is the result I get:
viewDidLoad
(294.0, 228.0)
(160.0, -64.0)
viewWillLayoutSubviews
(160.0, -64.0)
viewDidLayoutSubviews
(294.0, 228.0)
I don't have any constraints attached to this one view. Thoughts? I guess I could store the location and constantly move it around manually, but I'm sure there is a switch somewhere that turns off the autolayout for specific items. I just can't find it.
Thanks for your help...
The image must have autoresizingMasks that are getting translated into constraints.
Disable it by doing this:
image.translatesAutoresizingMaskIntoConstraints = false

How to maintain a minimum height of content size of UIScrollView?

I have a UIScrollView A (in fact a UICollectionView) filling the screen inside a UINavigationController B. The controller B's adjustScrollViewInsets is set to true.
I want to hide the navigation bar when user scrolls up, and show it when down. Following is my code:
func scrollViewDidScroll(scrollView: UIScrollView) {
if (self.lastContentOffset < scrollView.contentSize.height - scrollView.frame.size.height && self.lastContentOffset > scrollView.contentOffset.y) {
// dragging down
if self.navigationController!.navigationBarHidden {
self.navigationController?.setNavigationBarHidden(false, animated: true)
}
} else if (self.lastContentOffset > 0 && self.lastContentOffset < scrollView.contentOffset.y) {
// dragging up
if !self.navigationController!.navigationBarHidden {
self.navigationController?.setNavigationBarHidden(true, animated: true)
}
}
self.lastContentOffset = scrollView.contentOffset.y
}
Now the problem is, since the screen of iPhone 6+ is too large, the contentSize of the scroll view A is smaller than its frame(i.e. the full screen frame) when the navigation bar is hidden. In such circumstance, the scroll view will not be scrollable, and the navigation bar will never be back again.
I want to manually maintain the height of the contentSize of A to screen at least height + 1, but don't know how to do this. Could anyone help? Or provide a better solution?
BTW, I am using iOS 8 and Swift.
Lets say you need to keep the minimum content size of scroll view to 100(of course this will be dynamic and vary according to device)
NSInteger minScrollViewContentHeight = 100;
After populating the scroll view with content, you need to check if the scroll view's content size is less than minimum required scroll views content size. If its lesser than the required content size than you need to set the minimum content size of the scroll view as follows -
if(scrollView.contentSize.height < minScrollViewContentHeight)
[scrollView setContentSize:CGSizeMake(scrollView.frame.size.width, minScrollViewContentHeight)];
Off the top of my head (I'm on a phone), contentSize is not read-only I think.
How about changing it manually to the desired amount depending on the circumstances of scrolling direction etc?
Something like:
IF navbar is hidden THEN contentSize = whatever
An option would be to use the appearance and disappearance of cells to trigger the show/hide.
Use the delegate methods collectionView:willDisplayCell:forItemAtIndexPath: and collectionView:didEndDisplayingCell:forItemAtIndexPath: to detect movement. You can work out the direction from the index change of the cells being shown or removed. If you cannot scroll off screen then nothing happens.
You have to change no offset (which is actually just scrolling position), but contentSize itself. That means, that when you hide navigation bar, increase contentSize by navigation height (don't remember numbers) and when you show navigation bar, decrease contentSize. Or... Use AutoLayout and layoutIfNeeded method after showing/hiding navigation bar.
I stumbled upon a similar problem. I needed a minimum scrollable area for a tableview i was using.
ScrollView might be a bit easier since you can directly modify the contentView size.
If you're using autoLayout, try adding equal heights constraint between the contentView and the scrollView itself. Something along the lines of contentView.height = scrollView.height + scrollMin;
For my tableView i had to subclass UITableView and override the contentSize setter.
#define MIN_SCROLL 60
- (void)setContentSize:(CGSize)contentSize {
//take insets into account
UIEdgeInsets insets = self.contentInset;
CGFloat minHeight = self.frame.size.height - insets.top - insets.bottom + MIN_SCROLL;
if(contentSize.height < minHeight) {
contentSize.height = minHeight;
}
[super setContentSize:contentSize];
}

Resources