UIScrollView adjusts contentOffset when contentSize changes - ios

I am adjusting a detail view controller's state, just before it is pushed on a navigationController:
[self.detailViewController detailsForObject:someObject];
[self.navigationController pushViewController:self.detailViewController
animated:YES];
In the DetailViewController a scrollView resides. Which content I resize based on the passed object:
- (void)detailsForObject:(id)someObject {
// set some textView's content here
self.contentView.frame = <rect with new calculated size>;
self.scrollView.contentSize = self.contentView.frame.size;
self.scrollView.contentOffset = CGPointZero;
}
Now, this all works, but the scrollView adjusts it's contentOffset during the navigationController's slide-in animation. The contentOffset will be set to the difference between the last contentSize and the new calculated one. This means that the second time you open the detailsView, the details will scroll to some unwanted location. Even though I'm setting the contentOffset to CGPointZero explicitly.
I found that resetting the contentOffset in - viewWillAppear has no effect. The best I could come up with is resetting the contentOffset in viewDidAppear, causing a noticeable up and down movement of the content:
- (void)viewDidAppear:(BOOL)animated {
self.scrollView.contentOffset = CGPointZero;
}
Is there a way to prevent a UIScrollView from adjusting its contentOffset when its contentSize is changed?

Occurs when pushing a UIViewController containing a UIScrollView using a UINavigationController.
iOS 11+
Solution 1 (Swift Code):
scrollView.contentInsetAdjustmentBehavior = .never
Solution 2 (Storyboard)
iOS 7
Solution 1 (Code)
Set #property(nonatomic, assign) BOOL automaticallyAdjustsScrollViewInsets to NO.
Solution 2 (Storyboard)
Uncheck the Adjust Scroll View Insets
iOS 6
Solution (Code)
Set the UIScrollView's property contentOffset and contentInset in viewWillLayoutSubviews. Sample code:
- (void)viewWillLayoutSubviews{
[super viewWillLayoutSubviews];
self.scrollView.contentOffset = CGPointZero;
self.scrollView.contentInset = UIEdgeInsetsZero;
}

The cause of this problem remains unclear, though I've found a solution. By resetting the content size and offset before adjusting them, the UIScrollView won't animate:
- (void)detailsForObject:(id)someObject {
// These 2 lines solve the issue:
self.scrollView.contentSize = CGSizeZero;
self.scrollView.contentOffset = CGPointZero;
// set some textView's content here
self.contentView.frame = <rect with new calculated size>;
self.scrollView.contentSize = self.contentView.frame.size;
self.scrollView.contentOffset = CGPointZero;
}

I had the same issue with a UIScrollview, where the problem was caused by not setting the contentSize. After setting the contentSize to the number of items this problem was solved.
self.headerScrollView.mainScrollview.contentSize = CGSizeMake(320 * self.sortedMaterial.count, 0);

Here's what worked for me:
In the storyboard, in the Size Inspector for the scrollView, set Content Insets Adjustment Behavior to "Never".

Is your scrollView the root view of the DetailViewController? If yes, try wrapping the scrollView in a plain UIView and make the latter the root view of DetailViewController. Since UIViews don't have a contentOffset property, they are immune to content offset adjustments made by the navigation controller (due to the navigation bar, etc.).

I experienced the problem, and for a specific case - I don't adjust the size - I used the following:
float position = 100.0;//for example
SmallScroll.center = CGPointMake(position + SmallScroll.frame.size.width / 2.0, SmallScroll.center.y);
Same would work with y: anotherPosition + SmallScroll.frame.size.height / 2.0
So if you don't need to resize, this is a quick and painless solution.

I was experiencing a similar problem, where UIKit was setting the contentOffset of my scrollView during push animations.
None of these solutions were working for me, maybe because I was supporting iOS 10 and iOS 11.
I was able to fix my issue by subclassing my scrollview to keep UIKit from changing my offsets after the scrollview had been removed from the window:
/// A Scrollview that only allows the contentOffset to change while it is in the window hierarchy. This can keep UIKit from resetting the `contentOffset` during transitions, etc.
class LockingScrollView: UIScrollView {
override var contentOffset: CGPoint {
get {
return super.contentOffset
}
set {
if window != nil {
super.contentOffset = newValue
}
}
}
}

Adding to KarenAnne's answer:
iOS 11+
automaticallyAdjustsScrollViewInsets was deprecated
Use this istead:
Storyboards:
Code (Swift):
scrollView.contentInsetAdjustmentBehavior = .never

Related

Where is the proper place to set contentOffset in UIScrollView sub-class?

I am working on a view which inherits from UIScrollView, and the requirement is that it should start at a contentOffset.y position that is dependent on the view size. Specifically I want to start one screen down in a content that is 3 x the view height.
Like this:
- (void)configureStartCondition {
self.contentSize = CGSizeMake(self.bounds.size.width, self.bounds.size.height * 3.0);
self.contentOffset = CGPointMake(0.0, self.bounds.size.height * 1.0);
}
The view itself is wired up with constraints in Storyboard, just like any view. As it works, the framework will initially give the view the size it has in the storyboard, then when the device size is known, the view's size will be changed to its final size. This is how it should work, and I am fine with this. My question is where do I call configureStartCondition?
An obvious solution would be to put this code in setFrame:, but it doesn't work. setFrame: is only called for the initial frame size, which might or might not be the final size. Why is this?
// NOT working
- (void)setFrame:(CGRect)frame {
[super setFrame:frame];
[self configureStartCondition];
}
A more common place would be in layoutSubview, where I usually do this kind of setup. However, as it is a UIScrollView the layoutSubview is called very frequently as the user scrolls the view. Meaning I would need to save the last height and compare it to make things work, then run through this test millions of times just to be able to initialize. It feels like a kludge to me.
// Working, but ugly
- (void)layoutSubview {
[super layoutSubviews];
if (self.bounds.size.height != self.savedHeight) {
self.savedHeight = self.bounds.size.height;
[self configureStartCondition];
}
// Do layout stuff
}
Another place that may seem good is setBounds:. It will get called for the view size change, but since the contentOffset property is tied to the bounds property, I actually get as many calls here as to layoutSubviews.
So, is there a better place to do it, or a better way to do it?
Side issue, less important in my case, but can the content offset be set from a storyboard?
EDIT: Solutions in Swift are also fine.
setContentSize: works perfect for me. It called only when size changed.
PS: My code to check: (sorry for swift but UIKit make no difference)
class CustomScrollView: UIScrollView {
override var contentSize: CGSize{
didSet{
var offset = contentOffset
offset.y = contentSize.height / 2.0
contentOffset = offset
}
}
}

iOS11 customize navigation bar height

First of all, thank you for coming here and help solving my problem. Thank you!!!
In iOS11 beta6, sizeThatFits: seems to not work on UINavigationBar. I notice that UINavigationBar structure has changed by Reveal my app.
I have tried my best to change custom navigation bar's height. But it seems always to be 44, and it works before iOS11.
- (CGSize)sizeThatFits:(CGSize)size {
CGSize newSize = CGSizeMake(self.frame.size.width, 64);
return newSize;
}
Oddly, I just log its frame in didMoveToSuperview method, its height is 64, but I really see that is 44 in Reveal and app.
I have no idea about this... Help me please.. Thank you.
Update:
I found that about my custom navigation bar LayoutConstraints log in console like this :
"<NSAutoresizingMaskLayoutConstraint:0x604000495ae0 FDCustomNavigationBar:0x7fe2f01399d0.(null) == 42>",
"<NSAutoresizingMaskLayoutConstraint:0x604000495b30 FDCustomNavigationBar:0x7fe2f01399d0.height == 44>"`
bug I even no use auto layout in my navigation bar. What's wrong with it?
Update 8/28 :
I have set my custom navigation bar's subviews frame at navigation bar's -
layoutSubviewsmethod.
- (void)layoutSubviews {
[super layoutSubviews];
self.frame = CGRectMake(0, 0, CGRectGetWidth(self.frame), 64);
for (UIView *view in self.subviews) {
if([NSStringFromClass([view class]) containsString:#"Background"]) {
view.frame = self.bounds;
} else if ([NSStringFromClass([view class]) containsString:#"ContentView"]) {
CGRect frame = view.frame;
frame.origin.y = 20;
frame.size.height = self.bounds.size.height - frame.origin.y;
view.frame = frame;
}
}
}
but the navigation bar will cover view controller's view. How can I fix that?
Since iOS 11 UINavigationBar fully supports Auto Layout (this is the reason why you're seeing its constraints). I've opened a radar to Apple because I thought that setting a height constraint to the titleView would have adjusted the navigation bar height accordingly. However this is what Apple replied:
Full support for auto layout does not imply that your view can influence other aspects of the layout of the navigation bar – in particular, the navigation bar enforces its own height and does not allow the title view or other custom views to exceed the height of the navigation bar. We are continuing to work on this issue, and will follow up with you again.
As of today the radar is still open.
Hello I just experienced this same issue.
Now the top layout guide is deprecated on iOS 11. You have to reference the safeAreaLayoutGuide in your constraints.
Here's an example in swift
if #available(iOS 11, *) {
let guide = self.view.safeAreaLayoutGuide.topAnchor
let height = (self.navigationController?.navigationBar.frame.height)! - CGFloat(12)
NSLayoutConstraint.activate([
self.yourTableView.topAnchor.constraint(equalTo: guide, constant: height)
])
}
As you can see your view's top anchor should match the safeAreaLayoutGuide top anchor. In this example I'm using the variable height to create the new constraint. The variable height contains the navigation bar height minus a constant.
You should try by changing the height value.
Hope this helps you.

iOS 8 - Initially hide a UITableView's headerView from view

Just like in the native iOS Mail app, when I push a UITableViewController onto a UINavigationController, I would like to make it so that the UITableView initially appears slightly scrolled downwards, obscuring its headerView beneath the navigation controller's navigation bar.
At the same time, even if the height of all of the cells is smaller than the height of the table view, it should be possible for the user to scroll up and down to explicitly show or hide the header view again.
With that logic, it would appear that there are two considerations to make for this implementation:
1) Ensuring that the minimum content size of the table view is at least the height of the table view's frame + the height of the header view.
2) When the table view is initially presented, the content offset is incremented by the height of the header view.
I've tried manually setting both the contentOffset and contentSize properties of the table view in 'viewWillAppear', however this appears to have no effect (It's possible the table view is getting reloaded after that point). Trying to set them in 'viewDidAppear' will work, but that's too late as it only gets called once the 'push' animation has completed.
While this sort of question has been asked before for previous iOS versions, I was unable to get any of them working in iOS 8. Additionally, they all dealt with changing the offset, but not the contentSize of the table view.
Has anyone gotten this sort of behavior working in iOS 7 and/or 8 before?
Update - (30/1/2015)
Alright. This wasn't sitting well with me last night, so I had another play with it, and I found a MUCH better and cleaner solution.
I discovered that the tableView property of UITableViewController is NOT readonly. So it actually makes more sense to simply manage the contentSize property in a UITableView subclass and then assign that subclass back to the UITableViewController.
#implementation TOCustomTableView
- (void)setContentSize:(CGSize)contentSize
{
CGFloat scrollInset = self.contentInset.top + self.contentInset.bottom;
CGFloat height = (CGRectGetHeight(self.bounds) - scrollInset) + CGRectGetHeight(self.tableHeaderView.frame);
contentSize.height = MAX(height, contentSize.height);
[super setContentSize:contentSize];
}
#end
---
#implementation TOCustomTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView = [[TOCustomTableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
}
#end
This way, the table view's minimum contentSize is always explicitly set to be the height of the table view + the headerView size, achieving the desired effect with zero jittering. :)
Original Answer
trick14 pointed me in the right direction. So the correctly functioning code I ended up with.
- (void)resetTableViewInitialOffset
{
CGPoint contentOffset = self.tableView.contentOffset;
contentOffset.y = self.tableView.contentInset.top + CGRectGetHeight(self.headerView.frame);
self.tableView.contentOffset = contentOffset;
}
- (void)resetTableViewContentSize
{
CGSize contentSize = self.tableView.contentSize;
CGFloat scrollInset = self.tableView.contentInset.top + self.tableView.contentInset.bottom;
CGFloat height = (CGRectGetHeight(self.view.bounds) - scrollInset) + CGRectGetHeight(self.headerView.frame);
contentSize.height = MAX(height, contentSize.height);
self.tableView.contentSize = contentSize;
}
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
if (!self.headerBarInitiallyHidden) {
[self resetTableViewContentSize];
[self resetTableViewInitialOffset];
self.headerBarInitiallyHidden = YES;
}
}
I'm also making sure to call 'resetTableViewContentSize' each time I perform a 'reloadData' on the table view as well.

What do I have to do after setting contentInset or automaticallyAdjustsScrollViewInsets for it to actually beeing applied?

I have got an UIViewController containing an UIScrollView. Initially automaticallyAdjustsScrollViewInsets is not set (so it should default to YES).
On some user interaction I set automaticallyAdjustsScrollViewInsets to NO, so I can set my own contentInset on the scroll view.
After doing so nothing happens. What do I have to call afterwards?
Calling setNeedsDisplay on the scroll view doesn’t help.
If you want your initial display of your scrollview to include an initial content offset, then set the contentInset value in viewDidLoad.
If you wish to dynamically change the contentInset and have the contentOffset change as well, then you need to adjust the contentOffset at the same time as the contentInset. For example:
// Adjust content inset and initial offset to account for the header
UIEdgeInsets contentInset = self.collectionView.contentInset;
contentInset.top += self.stickyHeaderView.bounds.size.height;
self.collectionView.contentInset = contentInset;
if (-1 * self.collectionView.contentOffset.y < contentInset.top) {
CGPoint contentOffset = self.collectionView.contentOffset;
contentOffset.y = -1*contentInset.top;
[self.collectionView setContentOffset:contentOffset animated:YES];
}

iOS7: UICollectionView appearing under UINavigationBar

I've been building on iOS 7 for a while now but I've yet to get this solved, I have a number of views with autolayout enabled that were created in Storyboard and are displayed with a standard UINavigationController. The majority are fine, but the ones based on UICollectionView always place themselves under the navigation bar, unless I set the translucency to NO. I've tried the edgesExtended trick but that doesn't seem to solve it, I don't necessarily mind having the translucency off but I'd like to solve it cleaner.
FYI if your UICollectionView is the root view in your view controller's hierarchy and your view controller has automaticallyAdjustsScrollViewInsets set to YES (this is the default), then the contentInset should update automatically.
However, the scrollview's contentInset is only automatically updated if your scrollview (or tableview/collectionview/webview btw) is the first view in their view controller's hierarchy.
I often add a UIImageView first in my hierarchy in order to have a background image. If you do this, you have to manually set the edge insets of the scrollview in viewDidLayoutSubviews:
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat top = self.topLayoutGuide.length;
CGFloat bottom = self.bottomLayoutGuide.length;
UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
self.collectionView.contentInset = newInsets;
}
I had this problem before, just set the edge insents of the collection view with a top margin:
[self.myCollectionVC.collectionView setContentInset:UIEdgeInsetsMake(topMargin, 0, 0, 0)];
Where topMargin is the size of the nav bar, or whatever point you want the collection to start scrolling.
In this way, your collection view will start scrolling just below the navigation bar, and at the same time it will fill the whole screen and you will see it if your nav bar is translucent.
I had this problem after ios 11, just set the contentInsetAdjustmentBehavior of UICollectionView to never:
self.collectionView.contentInsetAdjustmentBehavior = .never
I am using swift and xcode 7.3.1. I solved it by going to story board and selecting my Navigation Controller and then unchecking "Extend Edges" "Under Top Bards".
-(void) viewDidLoad{
[super viewDidLoad];
self.automaticallyAdjustsScrollViewInsets = NO; //added important
}
- (void) viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
CGFloat top = self.topLayoutGuide.length;
CGFloat bottom = self.bottomLayoutGuide.length;
UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
self.collectionView.contentInset = newInsets;
}

Resources