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

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.

Related

Adjusting Container View's Height to Match Embedded UITableView

Using Storyboards and Autolayout, I have a UIViewController with a UIScrollView as the main view. I have several container views embedded in the scroll view. Some of those embedded container views contain UITableViews, each having cells of different heights. I'll need the tableView's height to be large enough to show all cells at once, as scrolling will be disabled on the tableView.
In the main UIViewController, container view's height has to be defined in order for the scroll view to work properly. This is problematic because there's no way for me to know how large my tableView will be once all it's cells of varying heights are finished rendering. How can I adjust my container view's height at runtime to fit my non-scrolling UITableView?
So far, I've done the following:
// in embedded UITableViewController
//
- (void)viewDidLoad {
// force layout early so I can determine my table's height
[self.tableView layoutIfNeeded];
if (self.detailsDelegate) {
[self.detailsTableDelegate didDetermineHeightForDetailsTableView:self.tableView];
}
}
// in my main UIViewController
// I have an IBOutlet to a height constraint set up on my container view
// this initial height constraint is just temporary, and will be overridden
// once this delegate method is called
- (void)didDetermineHeightForDetailsTableView:(UITableView *)tableView
{
self.detailsContainerHeightConstraint.constant = tableView.contentSize.height;
}
This is working fine and I was pleased with the results. However, I have one or two more container views to add, which will have non-scrolling tableViews, and I'd hate to have to create a new delegate protocol for each container view. I don't think I can make the protocol I have generic.
Any ideas?
Here's what I ended up doing:
// In my embedded UITableViewController:
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 60.0;
// via storyboards, this viewController has been embeded in a containerView, which is
// in a scrollView, which demands a height constraint. some rows from our static tableView
// might not display (for lack of data), so we need to send our table's height. we'll force
// layout early so we can get our size, and then pass it up to our delegate so it can set
// the containerView's heightConstraint.
[self.tableView layoutIfNeeded];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// in another embedded view controller:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.sizeForEmbeddingInContainerView = self.tableView.contentSize;
}
// then, in the parent view controller, I do this:
// 1) ensure each container view in the storyboard has an outlet to a height constraint
// 2) add this:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
self.placeDetailsContainerHeightConstraint.constant = self.placeDetailsTableViewController.sizeForEmbeddingInContainerView.height;
self.secondaryExperiencesContainerHeightConstraint.constant = self.secondaryExperiencesViewController.sizeForEmbeddingInContainerView.height;
}
I haven't done this yet, but it'd probably be best to create a Protocol with a property of CGSize sizeForEmbeddingInContainerView that each child view controller can adopt.
Here's what worked for me perfectly.
- (void)updateSizeBasedOnChildViews {
// Set height of container to match embedded tableview
CGRect containerFrame = self.cardTableContainer.frame;
containerFrame.size.height = [[[self.cardTableContainer subviews] lastObject]contentSize].height;
self.cardTableContainer.frame = containerFrame;
// Set content height of scrollview according to container
CGRect scrollFrame = self.cardTabScrollView.frame;
scrollFrame.size.height = containerFrame.origin.y + containerFrame.size.height;
// + height of any other subviews below the container
self.cardTabScrollView.contentSize = scrollFrame.size;
}

Where does UICollectionView etc calculate content size so that I can permanently increase it?

I want to add a view to the bottom of the content view of both a collection view and table view (and hence is applicable to any kind of scroll view) and I also want to be able to scroll down to see this view e.g.:
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
// Observe change in content size so can move my view when
// content size changes (keep it at the bottom)
[self addObserver:self forKeyPath:#"contentSize"
options:(NSKeyValueObservingOptionPrior)
context:nil];
CGRect frame = CGRectMake(0, 0, 200, 30);
self.loadingView = [[UIView alloc] initWithFrame:frame];
[self.loadingView setBackgroundColor:[UIColor blackColor]];
[self addSubview:self.loadingView];
// Increase height of content view so that can scroll to my view.
self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height+30);
}
return self;
}
However when, for example, a cell is inserted the contentSize is recalculated and whilst my view is still visible at the bottom of the content size (due to being able to bounce the scroll view) I can no longer scroll to it.
How do I ensure that the content size stays, as in my code, 30 points taller?
An additional question is:
is there any other way to track content size other than observing it?
Thanks in advance.
I have tried:
- (void)layoutSubviews
{
[super layoutSubviews];
self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height+30);
}
However this causes all sorts of display issues.
If i understand correctly, you want to show a loading view in the tableView (f.e.) at the bottom. You could add an extra UITableViewCell containing this LoaderView to the tableView.
(Must change the numberOfRowsInTableView)
In another perspective for scrollViews: Use smaller bounds then the content itself, to make it scrollable. For example frame = fullscreen. At every cell adding or modification in subviews (adding) contentSize = content size + 30 px.
Try making a subclass of the scroll view and override the contentSize getter to return always 30 px more.
- (CGSize)contentSize {
CGSize customContentSize = super.contentSize;
customContentSize.height += 30;
return customContentSize;
}
(I'm writing the code by memory, there may be errors)

Locking a UISearchBar to the top of a UITableView

I want to get the behavior like this:
I have a UISearchBar, which is a table header view of my tableview. When scrolling the table, the search bar does indeed move, but if you scroll above the boundaries of the table, the search bar never stops touching the navigation bar.
I found a good answer here - Locking a UISearchBar to the top of a UITableView like Game Center
But it not works on iOS 6 - manipulations with table view header frame don't work
What can be the reason of this?
I found a solution, which works on iOS 6 and lower
Make a subclass of UITableView and override layoutSubviews method
- (void)layoutSubviews
{
[super layoutSubviews];
CGRect rect = self.tableHeaderView.frame;
rect.origin.y = MIN(0, self.contentOffset.y);
self.tableHeaderView.frame = rect;
}
If you use the following:
[_tableView setTableHeaderView:_searchBar];
When you scroll the table view beyond the first row, the search bar should also disappear rather than sticking to the top of the view.
Without seeing your code, I can only assume that perhaps you have added the UISearchBar as your section header rather than the table header?
That because iOS6 sdk not update when scrolling, maybe same optimize. so need to call tableview layoutSubviews when scroll.
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
UISearchBar *searchBar = searchDisplayController.searchBar;
CGRect rect = searchBar.frame;
rect.origin.y = MIN(0, scrollView.contentOffset.y);
[scrollView layoutSubviews];
searchBar.frame = rect;
}

iOS UIView scrolling UI - how to implement?

Forgive me to the obtuse title, as I'm unsure how to describe this question.
Recently many iOS apps utilise a scrolling UI design pattern which helps to maximise screen real-estate, typically hiding the header when the user scrolls downwards.
For example, Instragram's main view has the Instragram header at the top. Scrolling upwards on this view keeps the header fixed at the top, and the view bounces back normally to the top of the content. But scroll down and the header acts as part of the content, making way for an extra 44 points of vertical space.
Its probably that I haven't done much iOS work in a while, but I can't easily figure out how best to impliment this? Apologies for the terrible description.
If the header stays put no matter what, use a separate view on top of the scroll view.
If you use UITableView, you can use section headers.
EDIT Use this code:
- (void)scrollViewDidScroll:(UIScrollView*) scrollView
{
CGPoint offset = scrollView.contentOffset;
CGRect headerFrame = _headerView.frame;
if(offset.y > 0){
headerFrame.origin.y = offset.y;
}
else{
headerFrame.origin.y = 0.0;
}
[_headerView setFrame:headerFrame];
}
(Assumes _headerView is your header, sitting on top of the scroll view, not inside it. Also, both scroll view and header begin at the top of their parent view, y==0. Also, your view controller must be set up as delegate of the scroll view)
I just wrote this code from memory; haven't tested it but at most it should only need tweaking.
I tried ranReloaded's answer above but it seems that calling setFrame: on a UIScrollView stops the view from bouncing when going beyond its bounds.
Instead I set the scroll view to fit inside another UIView called scrollerWrapper. Applying the calculated origin and height to this view gives me effect I'm after plus retains the bounce behaviour.
- (void)scrollViewDidScroll:(UIScrollView*) scrollView
{
CGPoint offset = scrollView.contentOffset;
CGRect headerFrame = header.frame;
CGRect wrapperFrame = scrollerWrapper.frame;
if(offset.y > 0){
headerFrame.origin.y = -offset.y;
wrapperFrame.origin.y = MAX(0, headerFrame.size.height - offset.y);
}
else{
headerFrame.origin.y = 0.0;
wrapperFrame.origin.y = headerFrame.size.height;
}
wrapperFrame.size.height = self.view.frame.size.height - wrapperFrame.origin.y;
[header setFrame:headerFrame];
[scrollerWrapper setFrame:wrapperFrame];
}

UIScrollView adjusts contentOffset when contentSize changes

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

Resources