I am working on an app, which has implements a Search Display Controller. The search results table view, doesn't behave right all the time, and I'm trying to solve this issue from a few days. Here is a screen recording of the behavior.
I'm not modifying the frame or bounds of the table view. I'm only resigning first responder when there is no text in the search field, and calling the [searchDisplayController setActive: animated: ] method.
Please help me out.
Instead of setting the scrollIndicatorInsets and contentInsets of the searchResultsTableView when the keyboard hides, putting it within the 'willShowSearchResultsTableView:' delegate method fixed it for me:
- (void)searchDisplayController:(UISearchDisplayController *)controller willShowSearchResultsTableView:(UITableView *)tableView
{
[tableView setContentInset:UIEdgeInsetsZero];
[tableView setScrollIndicatorInsets:UIEdgeInsetsZero];
}
Solved this issue by using my own TableView instead of SearchResultsDisplayController's table view. Placed a new tableview on top of my previous tableview, and showing and hiding the search table view based on the delegates of the Search Bar (textDidChange).
I adopted this workaround, as I failed to solve the mysteries of layout problems in SearchResultsTableView.
I was getting what seems to be a very similar behaviour, but it only happened when running in the device (iPhone or iPad), but not in the simulator (both in iOS 8, Xcode 6.0.1, using Swift).
For some reason, when inputting a search text the contentInset.bottom and scrollIndicatorInsets.bottom of my searchResultsTableView didn't return to its original value after dismissing the keyboard. Each time the keyboard was shown and then dismissed, this value would increase by the keyboard's height and the space would get increasingly larger. I don't know what causes this, as I never fiddle with insets in the code. I'm suspecting it is some weird constraint going haywire, but I couldn't find it.
What solved for me was to programatically set the bottom insets to zero, whenever the keyboard was dismissed:
searchController.searchResultsTableView.contentInset.bottom = 0
searchController.searchResultsTableView.scrollIndicatorInsets.bottom = 0
I know this is only a workaround. Hope it helps in your case.
I'm doing very similar to #ricardoaraujo, but find that when I scroll back down to the bottom of the list, the last few cells are off the bottom of the screen (about 200px worth, therefore 1 x keyboard.height).
The frame of the searchResultsTableView is perfect (checked by both logging and using the new Debug View Hierarchy mode), and the contentInset is set to UIEdgeInsetsZero.
You'll probably therefore notice the same thing, but short of creating your own UITableView to display the results (as in #Vignesh's answer), there doesn't seem to be a nice way around it.
This does not happen on iOS7, so hopefully will be fixed in iOS8.0.3. Will check radar.
#Vignesh good that you got your problem solved, but UISearchDisplayController does that for us. It's true that it is a keyboard missing calculation issue, so I just had to set the contentInset and scrollIndicatorInsets back to UIEdgeInsetsZero when my keyboard goes way.
func searchDisplayController(controller: UISearchDisplayController, didHideSearchResultsTableView tableView: UITableView) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardDidHideNotification, object: nil)
}
func searchDisplayController(controller: UISearchDisplayController, willShowSearchResultsTableView tableView: UITableView) {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillHide", name: UIKeyboardDidHideNotification, object: nil)
}
func keyboardWillHide() {
let tableView = self.searchController.searchResultsTableView
tableView.contentInset = UIEdgeInsetsZero
tableView.scrollIndicatorInsets = UIEdgeInsetsZero
}
It's just another workaround, but it's working well so far on iOS7 +
Swift version:
func searchDisplayController(controller: UISearchDisplayController, willShowSearchResultsTableView table: UITableView!) -> Void {
table.contentInset = UIEdgeInsetsZero
table.scrollIndicatorInsets = UIEdgeInsetsZero
}
Based on answer of #user2795503
Related
I have a problem with my textViews, because when they appear, always appear scrolled to the bottom, I read here that is a problem that occurs when the textview has constraints, and the solution is set the isScrollEnabled to false and in the didAppear set to true, but because the superview of my textview is a cell in a collectionView, I don't know how to solve this
BTW, I'm using swift 3 and XCode 8
edit: Sorry, I'm not using a nib
I suggest to override prepareForReuse to make any changes before the cell will be visible.
I just solved this, but i did a lot of things, i'm new in ios programming and i don't understand very well a lot of things, but this is how i did it.
To make a textView scrolls to top in a collectionView in my view controller i search for any textViews there and when find it, i set the contentOffset, for the first cell i used the viewdidlayoutsubviews, but for the other cell i had to use the willDisplay and didEndDisplaying, making exactly the same:
let container = collectionView.subviews[0].subviews
for v in container {
if let fView = v as? UITextView {
fView.contentOffset = CGPoint(x: 0,y :0)
}
}
I enter to the cell subviews directly, and set is contentOffset, but the trick is to reload the cell in the cellForItemAt, just like this:
collectionView.reloadItems(at: [indexPath])
and thats it, i hope this helps some one and thanks for your answers.
I have a chat application which backs a single-section UITableViewController with a core data Messages table. Based on push messages, I update the status of a message or append new messages. When a new message arrives, I'd like to scroll to the bottom of the table. Each cell has variable height, which is driven by autolayout, using:
self.tableView.estimatedRowHeight = 76
self.tableView.rowHeight = UITableViewAutomaticDimension
I load the NSFetchRequest() in viewDidLoad, which loads the messages from a core data table, and jumps to the last message in viewDidAppear. This works properly when running from Xcode, but if I start the app on an iPhone instead, it doesn't jump to the last message, but somewhere in the middle. Also, if I defer the message to the main thread, it still doesn't work. However, if I insert even a small delay like this, it always works properly:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// not sure why this delay is needed
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.001 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
self.scrollToLastMessage()
}
}
Of course I don't like having a fixed delay, especially one I don't understand. Is there a specific event that I should hook for scrolling to the last message?
More vexing, I can't find a way to have my NSFetchResultsController .Insert (append) or .Update a row without the UITableViewController scrolling to another location. I've tried capturing and restoring the contentOffset, which I have to do animated: false to prevent very screwed up animations, but the table still has visible scrolling flicker. I've tried reloadData() for the whole table, reloadRowsAtIndexPaths() to just update the relevant cell, with and without beginUpdates() / endUpdates(). I also tried scrollToRowAtIndexPath(lastRow). No matter what, I can't get this simple update to work without the UITableView autonomously scrolling. It seems to scroll the changed row to the middle of the screen with animation, after which I have to scroll it back to where it should be. Has anyone else noticed this? Is this scrolling an artifact of NSFetchResultsController, a "feature" of UITableViewController or a result of using UITableViewAutomaticDimension?
So far I get the best results with the following, but there is visible flicker.
func controllerDidChangeContent(controller: NSFetchedResultsController!) {
self.tableView.reloadData()
}
private func scrollToLastMessage() {
dispatch_async(dispatch_get_main_queue()) {
// without the check below, this oddly doesn't scroll to the right place (autolayout issue?)
if self.tableView.contentSize.height > self.tableView.frame.size.height {
let offset = CGPoint(x: 0, y: self.tableView.contentSize.height - self.tableView.frame.size.height)
self.tableView.setContentOffset(offset, animated: false)
}
}
}
I notice when using UITableViewAutomaticDimension that the offset of the last row changes. For instance, if I ask for the offset at one time, in order to jump to the right contentOffset to display it, the contentOffset can spontaneously change as UITableViewController probes other cells for their heights and scrolls around. Is there a way to prevent this? I.e., to get UITableViewAutomaticDimension to always measure the real height of cells rather than the estimated height, so that I don't have to implement a rowHeight method? As mentioned above, I've also tried scrollToRowAtIndexPath(lastRow), but that has the same weird, scrolling behaviour.
tldr; Auto constrains appear to break on push segue and return to view for custom cells
Edit: I have provided a github example project that shows off the error that occurs
https://github.com/Matthew-Kempson/TableViewExample.git
I am creating an app which requires the title label of the custom UITableCell to allow for varying lines dependent on the length of the post title. The cells load into the view correctly but if I press on a cell to load the post in a push segue to a view containing a WKWebView you can see, as shown in the screen shot, the cells move immediately to incorrect positions. This is also viewed when loading the view back through the back button of the UINavigationController.
In this particular example I pressed on the very end cell, with the title "Two buddies I took a picture of in Paris", and everything is loaded correctly. Then as shown in the next screenshot the cells all move upwards for unknown reasons in the background of loading the second view. Then when I load the view back you can see the screen has shifted upwards slightly and I cannot actually scroll any lower than is shown. This appears to be random as with other tests when the view loads back there is white space under the bottom cell that does not disappear.
I have also included a picture containing the constraints that the cells has.
Images (I need more reputation to provide images in this question apparently so they are in this imgur album): http://imgur.com/a/gY87E
My code:
Method in custom cell to allow the cell to resize the view correctly when rotating:
override func layoutSubviews() {
super.layoutSubviews()
self.contentView.layoutIfNeeded()
// Update the label constaints
self.titleLabel.preferredMaxLayoutWidth = self.titleLabel.frame.width
self.detailsLabel.preferredMaxLayoutWidth = self.detailsLabel.frame.width
}
Code in tableview
override func viewDidLoad() {
super.viewDidLoad()
// Create and register the custom cell
self.tableView.estimatedRowHeight = 56
self.tableView.rowHeight = UITableViewAutomaticDimension
}
Code to create the cell
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell! {
if let cell = tableView.dequeueReusableCellWithIdentifier("LinkCell", forIndexPath: indexPath) as? LinkTableViewCell {
// Retrieve the post and set details
let link: Link = self.linksArray.objectAtIndex(indexPath.row) as Link
cell.titleLabel.text = link.title
cell.scoreLabel.text = "\(link.score)"
cell.detailsLabel.text = link.stringCreatedTimeIntervalSinceNow() + " ago by " + link.author + " to /r/" + link.subreddit
return cell
}
return nil
}
If you require any more code or information please ask and I shall provide what is necessary
Thanks for your help!
This bug is caused by having no tableView:estimatedHeightForRowAtIndexPath: method. It's an optional part of the UITableViewDelegate protocol.
This isn't how it's supposed to work. Apple's documentation says:
Providing an estimate the height of rows can improve the user experience when loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights and so lead to a longer load time. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
So this method is supposed to be optional. You'd think if you skipped it, it would fall back on the accurate tableView:heightForRowAtIndexPath:, right? But if you skip it on iOS 8, you'll get this behaviour.
What seems to be happening? I have no internal knowledge, but it looks like if you do not implement this method, the UITableView will treat that as an estimated row height of 0. It will compensate for this somewhat (and, at least in some cases, complain in the log), but you'll still see an incorrect size. This is quite obviously a bug in UITableView. You see this bug in some of Apple's apps, including something as basic as Settings.
So how do you fix it? Provide the method! Implement tableView: estimatedHeightForRowAtIndexPath:. If you don't have a better (and fast) estimate, just return UITableViewAutomaticDimension. That will fix this bug completely.
Like this:
- (CGFloat)tableView:(UITableView *)tableView
estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
There are potential side effects. You're providing a very rough estimate. If you see consequences from this (possibly cells shifting size as you scroll), you can try to return a more accurate estimate. (Remember, though: estimate.)
That said, this method is not supposed to return a perfect size, just a good enough size. Speed is more important than accuracy. And while I spotted a few scrolling glitches in the Simulator there were none in any of my apps on the actual device, either the iPhone or iPad. (I actually tried writing a more accurate estimate. But it's hard to balance speed and accuracy, and there was simply no observable difference in any of my apps. They all worked exactly as well as just returning UITableViewAutomaticDimension, which was simpler and was enough to fix the bug.)
So I suggest you do not try to do more unless more is required. Doing more if it is not required is more likely to cause bugs than fix them. You could end up returning 0 in some cases, and depending on when you return it that could lead to the original problem reappearing.
The reason Kai's answer above appears to work is that it implements tableView:estimatedHeightForRowAtIndexPath: and thus avoids the assumption of 0. And it does not return 0 when the view is disappearing. That said, Kai's answer is overly complicated, slow, and no more accurate than just returning UITableViewAutomaticDimension. (But, again, thanks Kai. I'd never have figured this out if I hadn't seen your answer and been inspired to pull it apart and figure out why it works.)]
Note that you may also need to force layout of the cell. You'd think iOS would do this automatically when you return the cell, but it doesn't always. (I will edit this once I investigate a bit more to figure out when you need to do this.)
If you need to do this, use this code before return cell;:
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
The problem of this behavior is when you push a segue the tableView will call the estimatedHeightForRowAtIndexPath for the visible cells and reset the cell height to a default value. This happens after the viewWillDisappear call. If you come back to TableView all the visible cells are messed up..
I solved this problem with a estimatedCellHeightCache. I simply add this code snipped to the cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
// put estimated cell height in cache if needed
if (![self isEstimatedRowHeightInCache:indexPath]) {
CGSize cellSize = [cell systemLayoutSizeFittingSize:CGSizeMake(self.view.frame.size.width, 0) withHorizontalFittingPriority:1000.0 verticalFittingPriority:50.0];
[self putEstimatedCellHeightToCache:indexPath height:cellSize.height];
}
...
}
Now you have to implement the estimatedHeightForRowAtIndexPath as following:
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self getEstimatedCellHeightFromCache:indexPath defaultHeight:41.5];
}
Configure the Cache
Add this property to your .h file:
#property NSMutableDictionary *estimatedRowHeightCache;
Implement methods to put/get/reset.. the cache:
#pragma mark - estimated height cache methods
// put height to cache
- (void) putEstimatedCellHeightToCache:(NSIndexPath *) indexPath height:(CGFloat) height {
[self initEstimatedRowHeightCacheIfNeeded];
[self.estimatedRowHeightCache setValue:[[NSNumber alloc] initWithFloat:height] forKey:[NSString stringWithFormat:#"%d", indexPath.row]];
}
// get height from cache
- (CGFloat) getEstimatedCellHeightFromCache:(NSIndexPath *) indexPath defaultHeight:(CGFloat) defaultHeight {
[self initEstimatedRowHeightCacheIfNeeded];
NSNumber *estimatedHeight = [self.estimatedRowHeightCache valueForKey:[NSString stringWithFormat:#"%d", indexPath.row]];
if (estimatedHeight != nil) {
//NSLog(#"cached: %f", [estimatedHeight floatValue]);
return [estimatedHeight floatValue];
}
//NSLog(#"not cached: %f", defaultHeight);
return defaultHeight;
}
// check if height is on cache
- (BOOL) isEstimatedRowHeightInCache:(NSIndexPath *) indexPath {
if ([self getEstimatedCellHeightFromCache:indexPath defaultHeight:0] > 0) {
return YES;
}
return NO;
}
// init cache
-(void) initEstimatedRowHeightCacheIfNeeded {
if (self.estimatedRowHeightCache == nil) {
self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
}
}
// custom [self.tableView reloadData]
-(void) tableViewReloadData {
// clear cache on reload
self.estimatedRowHeightCache = [[NSMutableDictionary alloc] init];
[self.tableView reloadData];
}
I had the exact same problem. The table view had several different cell classes, each of which was a different height. Moreover, one of the cells classes had to show additional text, meaning further variation.
Scrolling was perfect in most situations. However, the same problem described in the question manifested. That was, having selected a table cell and presented another view controller, on return to the original table view, the upwards scrolling was extremely jerky.
The first line of investigation was to consider why data was being reloaded at all. Having experimented, I can confirm that on return to the table view, data is reloaded, albeit not using reloadData.
See my comment ios 8 tableview reloads automatically when view appears after pop
With no mechanism to deactivate this behaviour, the next line of approach was to investigate the jerky scrolling.
I came to the conclusion that the estimates returned by estimatedHeightForRowAtIndexPath are an estimated precalculation. Log to console out the estimates and you'll see that the delegate method is queried for every row when the table view first appears. That's before any scrolling.
I quickly discovered that some of the height estimate logic in my code was badly wrong. Resolving this fixed the worst of the jarring.
To achieve perfect scrolling, I took a slightly different approach to the answers above. The heights were cached, but the values used were from the actual heights that would have been captured as the user scrolls downwards:
var myRowHeightEstimateCache = [String:CGFloat]()
To store:
func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
myRowHeightEstimateCache["\(indexPath.row)"] = CGRectGetHeight(cell.frame)
}
Using from the cache:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
{
if let height = myRowHeightEstimateCache["\(indexPath.row)"]
{
return height
}
else
{
// Not in cache
... try to figure out estimate
}
Note that in the method above, you will need to return some estimate, as that method will of course be called before didEndDisplayingCell.
My guess is that there is some sort of Apple bug underneath all of this. That's why this issue only manifests in an exit scenario.
Bottom line is that this solution is very similar to those above. However, I avoid any tricky calculations and make use of the UITableViewAutomaticDimension behaviour to just cache the actual row heights displayed using didEndDisplayingCell.
TLDR: work around what's most likely a UIKit defect by caching the actual row heights. Then query your cache as the first option in the estimation method.
Well, until it works, you can delete those two line:
self.tableView.estimatedRowHeight = 45
self.tableView.rowHeight = UITableViewAutomaticDimension
And add this method to your viewController:
override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
let cell = tableView.dequeueReusableCellWithIdentifier("cell") as TableViewCell
cell.cellLabel.text = self.tableArray[indexPath.row]
//Leading space to container margin constraint: 0, Trailling space to container margin constraint: 0
let width = tableView.frame.size.width - 0
let size = cell.cellLabel.sizeThatFits(CGSizeMake(width, CGFloat(FLT_MAX)))
//Top space to container margin constraint: 0, Bottom space to container margin constraint: 0, cell line: 1
let height = size.height + 1
return (height <= 45) ? 45 : height
}
It worked without any other changes in your test project.
If you have set tableView's estimatedRowHeight property.
tableView.estimatedRowHeight = 100;
Then comment it.
// tableView.estimatedRowHeight = 100;
It solved the bug which occurs in iOS8.1 for me.
If you really want to keep it,then you could force tableView to reloadData before pushing.
[self.tableView reloadData];
[self.navigationController pushViewController:vc animated:YES];
or do it in viewWillDisappear:.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.tableView reloadData];
}
Hope it helps.
In xcode 6 final for me the workaround does not work. I am using custom cells and dequeing a cell in heightForCell leads to infinity loop. As dequeing a cell calls heightForCell.
And still the bug seems to be present.
If none of the above worked for you (as it happened to me) just check the estimatedRowHeight property from the table view is kind of accurate. I checked I was using 50 pixels when it was actually closer to 150 pixels. Updating this value fixed the issue!
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = tableViewEstimatedRowHeight // This should be accurate.
Starting in iOS7, there is additional space at the top of my UITableView's which have a style UITableViewStyleGrouped.
Here is an example:
The tableview starts at the first arrow, there are 35 pixels of unexplained padding, then the green header is a UIView returned by viewForHeaderInSection (where the section is 0).
Can anyone explain where this 35-pixel amount is coming from and how I can get rid of it without switching to UITableViewStylePlain?
Update (Answer):
In iOS 11 and later:
tableView.contentInsetAdjustmentBehavior = .never
I was helped by the following:
YouStoryboard.storyboard > YouViewController > Attributes inspector > Uncheck - Adjust scroll view insets.
I played around with it a bit more and it seems like this is a side-effect of setting the tableView's tableHeaderView = nil.
Because my tableView has a dynamically appearing tableHeaderView, when I need to hide the tableHeaderView, instead of doing self.tableView.tableHeaderView = nil;, I do:
self.tableView.tableHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, self.tableView.bounds.size.width, 0.01f)];
I like this solution better than setting a somewhat arbitrary contentInset.top because I use the contentInset.top dynamically as well. Having to remember to remove an extra 35px whenever I recalculate contentInset.top is tedious.
Try changing the contentInset property that UITableView inherits from UIScrollView.
self.tableView.contentInset = UIEdgeInsetsMake(-20, 0, 0, 0);
It's a workaround, but it works
For IOS 7 if you are allocing a tableview in a view controller you may look into
self.edgesForExtendedLayout = UIRectEdgeNone;
your problem seemed similar to mine
Update:
Swift in iOS 9.x:
self.edgesForExtendedLayout = UIRectEdge.None
Swift 3 :
self.edgesForExtendedLayout = UIRectEdge.init(rawValue: 0)
self.automaticallyAdjustsScrollViewInsets = NO;
try, you can deal with it!
You could detect if your app is running iOS7 or greater and add this two methods in your table view delegate (usually in your UIViewController code)
-(CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return CGFLOAT_MIN;
}
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return CGFLOAT_MIN;
}
This maybe is not an elegant solution but works for me
Swift version:
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
override func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
Solution for iOS 15:
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
To fix in a whole project:
if #available(iOS 15.0, *) {
UITableView.appearance().sectionHeaderTopPadding = 0
}
More details: Extra padding above table view headers in iOS 15
Note: This only applies to UITableView.Style.plain.
I have found the cause of my original bug and created a sample project showcasing it. I believe there is an iOS7 bug.
As of iOS7, if you create a UITableView with the Grouped style, but do not have a delegate set on first layout, then you set a delegate and call reloadData, there will be a 35px space at the top that will never go away.
See this project I made showcasing the bug: https://github.com/esilverberg/TableViewDelayedDelegateBug
Specifically this file: https://github.com/esilverberg/TableViewDelayedDelegateBug/blob/master/TableViewDelayedDelegateBug/ViewController.m
If line 24 is active,
[self performSelector:#selector(updateDelegate) withObject:nil afterDelay:0.0];
there will be an extra 35 px space at the top. If line 27 is active and 24 is commented out,
self.tableView.delegate = self;
no space at the top. It's like the tableView is caching a result somewhere and not redrawing itself after the delegate is set and reloadData is called.
Uncheck "Adjust Scroll View insets"
Another quick comment... even in XCode 6.1, there is a bug with vertical spaces appearing at the top of UIScrollViews, UITextViews and UITableViews.
Sometimes, the only way to fix this issue is to go into the Storyboard and drag the problem control so it's no longer the first subview on the page.
(My thanks to Oded for pointing me in this direction... I'm posting this comment, just to add a few screenshots, to demonstrate the symptoms and fix.)
While using grouped TableView use this to avoid border cutting in viewWillAppear
self.tableView.contentInset = UIEdgeInsetsMake(-35, 0, 0, 0);
According to this transition guide for iOS7 by Apple, the scroll view’s content insets is automatically adjusted.
The default value of automaticallyAdjustsScrollViewInsets is set to YES.
The UIViewController which has the UITableView should set this property to NO.
self.automaticallyAdjustsScrollViewInsets = NO;
This will do the trick.
EDIT 1:
Also, one could try -
self.navigationController.navigationBar.translucent = YES;
This also removes the extra padding on the top.
A lot of the previous answers above are too hacky. They would break at anytime in the future if Apple decides to fix this unexpected behavior.
Root of the issue:
a UITableView doesn't like to have a header with a height of 0.0. If what's you're trying to do is to have a header with a height of 0, you can jump to the solution.
even if later you assign a non 0.0 height to your header, a UITableView doesn't like to be assigned a header with a height of 0.0 at first.
Solution:
Then, the most simple and reliable fix is to ensure that your header height is not 0 when you assign it to your table view.
Something like this would work:
// Replace UIView with whatever class you're using as your header below:
UIView *tableViewHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, self.tableView.bounds.size.width, CGFLOAT_MIN)];
self.tableView.tableHeaderView = tableViewHeaderView;
Something like this would lead to the issue at some point (typically, after a scroll):
// Replace UIView with whatever class you're using as your header below:
UIView *tableViewHeaderView = [[UIView alloc] initWithFrame:CGRectZero];
self.tableView.tableHeaderView = tableViewHeaderView;
Storyboard:
Just uncheck: Adjust Scroll View Insets in View Controller's options
Code:
self.automaticallyAdjustsScrollViewInsets = false
This is the solution for iOS 10 using Swift 3:
You can get rid of top and bottom paddings by implementing the following methods from the UITableViewDelegate.
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
return CGFloat.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
{
return CGFloat.leastNormalMagnitude
}
This code worked for me, The best answer for me that was written in objective-C at up-side so I converted it into Swift.
For Swift 4.0+
self.tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: self.tableView.bounds.size.width, height: .leastNonzeroMagnitude))
Just write this into viewDidLoad() and it will work like a charm.
For iOS 15+, above one won't work, so use this:-
if #available(iOS 15.0, *) {
tableView.sectionHeaderTopPadding = 0
}
For iOS 15+, if you want to apply change for your whole project, so use this:-
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 15.0, *) {
UITableView.appearance().sectionHeaderTopPadding = 0.0
}
}
So I was trying every method here, and this time none of them helped. My case was a grouped table view on iOS 9. I don't really know why and how I found out this one, but for me, setting the tableViewHeader with a UIView with at least 0.01 height worked out. CGRectZero didn't help, nothing really helped:
tableView.tableHeaderView = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 0.0, height: 0.01))
Simply add the following to your viewDidLoad in your VC:
self.automaticallyAdjustsScrollViewInsets = NO;
In my case this was what helped me. I'm supporting ios6 also.
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) {
self.edgesForExtendedLayout = UIRectEdgeNone;
self.extendedLayoutIncludesOpaqueBars = NO;
self.automaticallyAdjustsScrollViewInsets = NO;
}
This is how it can be fixed easily in iOS 11 and Xcode 9.1 through Storyboard:
Select Table View > Size Inspector > Content Insets: Never
Swift: iOS I had tableview on scroll view .. when I was click "Back" on the same screen. Scroll view take more space on top.. to solve this I have used :
self.automaticallyAdjustsScrollViewInsets = false
A Boolean value that indicates whether the view controller should automatically adjust its scroll view insets.
Default value is true, which allows the view controller to adjust its scroll view insets in response to the screen areas consumed by the status bar, navigation bar, and toolbar or tab bar. Set to false if you want to manage scroll view inset adjustments yourself, such as when there is more than one scroll view in the view hierarchy.
To be specific, to remove tableviewHeader space from top i made these changes:
YouStoryboard.storyboard > YouViewController > Select TableView > Size inspector > Content insets - Set it to never.
Thanks to the answer by #Aurelien Porte. Here is my solution
Cause of this issue:-
a UITableView doesn't like to have a header with a height of 0.0. If what's you're trying to do is to have a header with a height of 0, you can jump to the solution.
even if later you assign a non 0.0 height to your header, a UITableView doesn't like to be assigned a header with a height of 0.0 at first.
In ViewDidLoad:-
self.edgesForExtendedLayout = UIRectEdge.None
self.automaticallyAdjustsScrollViewInsets = false
No Need For Something Like This :-
self.myTableview.contentInset = UIEdgeInsetsMake(-56, 0, 0, 0)
In heightForHeaderInSection delegate:-
if section == 0
{
return 1
}
else
{
return 40; // your other headers height value
}
In viewForHeaderInSection delegate :-
if section == 0
{
// Note CGFloat.min for swift
// For Objective-c CGFLOAT_MIN
let headerView = UIView.init(frame: CGRectMake(0.0, 0.0, self.myShaadiTableview.bounds.size.width, CGFloat.min))
return headerView
}
else
{
// Construct your other headers here
}
I'm assuming that is just part of the new UITableViewStyleGrouped styling. It is in all grouped table views and there doesn't seem to be any direct way to control that space.
If that space is being represented by a UIView, it would be possible to search through all the subviews of the UITableView to find that specific view and edit it directly. However, there is also the possibility that that space is just a hardcoded offset before headers and cells start and there won't be any way to edit it.
To search through all subviews (I would run this code when the table has no cells, to make it a little easier to read the output):
- (void)listSubviewsOfView:(UIView *)view {
// Get the subviews of the view
NSArray *subviews = [view subviews];
// Return if there are no subviews
if ([subviews count] == 0) return;
for (UIView *subview in subviews) {
NSLog(#"%#", subview);
// List the subviews of subview
[self listSubviewsOfView:subview];
}
}
My answer is going to be more general answer, but can be applied on this as well.
If the root view (of the ViewController) or the first child (subview) of the root view is subclass of the UIScrollView (or UIScrollView itself), and if
self.navigationController.navigationBar.translucent = YES;
framework will automatically set pre-calculated contentInset.
To avoid this you can do
self.automaticallyAdjustsScrollViewInsets = NO;
but in my case I wasn't able to do this, because I was implementing SDK which has UIView component which can be used by other developers. That UIView component contains UIWebView (which has UIScrollView as the first subview). If that component is added as the first child in the UIViewController's view hierarchy, automatic insets will be applied by system.
I've fixed this by adding dummy view with frame (0,0,0,0) before adding UIWebView.
In this case system didn't find subclass of the UIScrollView as the first subview and didn't apply insets
The only thing that worked for me was:
Swift:
tableView.sectionHeaderHeight = 0
tableView.sectionFooterHeight = 0
Objective-C:
self.tableView.sectionHeaderHeight = 0;
self.tableView.sectionFooterHeight = 0;
Also, I still had an extra space for the first section. That was because I was using the tableHeaderView property incorrectly. Fixed that as well by adding:
self.tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: tableView.frame.size.width, height: 0.01))
Swift 4 code:
For tableview with no section headers you can add this code:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return CGFloat.leastNormalMagnitude
}
and you will get the header spacing to 0.
If you want a header of your specific height pass that value:
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return header_height
}
and the view from viewForHeaderinSection delegate.
2022 answer:
You just do this
tableView.contentInsetAdjustmentBehavior = .never
which is undocumented
Bizarre subtle gotchya ->
Tableviews have a very strange behavior these days:
On devices with a notch (XR, etc) it will without telling you add more inset BUT ONLY IF the table starts at the physical top of the screen.
If you start NOT at the top of the screen, it won't do that, but
Both of those cases are >> unrelated << to safeAreaInsets ....... which is very confusing
All of that is totally undocumented ... you can waste hours figuring this out.
If you do need your measurements to start actually from the top of the screen/table,
in fact simply go:
tableView.contentInsetAdjustmentBehavior = .never
A good example is obviously when you add some sort of banner or similar thing over the top of a table, which is common these days, and you just set the top inset of the table to whatever height your banner/etc becomes when it's running.
To do that, you must use the
tableView.contentInsetAdjustmentBehavior = .never
call :/
Bonus gotchya
Don't forget that almost always these days, you're loading some information (user pictures, description, whatever) dynamically, so you can't set such values to the final needed value until the info arrives. Another gotchya. :/
So you'd have code like:
func setTableOffsetOnceFlagAreaSizeIsKnown() {
tableView.contentInset.top = yourSpecialFlagViewUpTop.bounds.height
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
setTableOffsetOnceFlagAreaSizeIsKnown()
}
I had the same fix as arielyz. Once I moved the UITableView to be not the first subview of the parent view, it went away. My space was 20 px, not 35.
I wasn't able to recreate it in a portrait xib, only a landscape xib. I'll file a radar bug later if I can reproduce it in a simple demo app.
I think making UIEdgeInsets -35 0 0 0 is tedious. In my case, I implemented tableView: heightForHeaderInSection: method and it has a potential to return 0.
When I changed 0 to 0.1f, the problem just went away.
I have dragged a plain jane UITableView onto a UIViewController in iOS 7.
Now there is an vertical offset of space before the first cell starts. How do I get rid of it? I want the first line to be much closer to the top edge of where the UITableView actually starts. I did not ask for the large offset did I?
Any ideas?
The new iOS 7 implementation of UIViewController has a new set of options that allows the developer to choose if the system will automatically add insets for UIScrollView, UITableView and derivations.
To disable this behaviour uncheck these boxes for all your wanted UIViewControllers in InterfaceBuilder, on UIViewController selected object inspector:
For more details:
Submit your iOS 7 apps today.
iOS 7 UI Transition Guide > Appearance and Behavior
By default table view controllers will pad the content down under the nav bar so you could scroll the content under it and see it, in a blurred state, underneath the navbar/toolbar.
Looks like you're positioning it at 44 (maybe 64)px to move it out from under the nav bar, but it already compensates for this so you get a big gap.
Go to the storyboard/xib in IB and untick the show content under nav bar stuff.
From iOS7 transition guide:
If you don’t want a scroll view’s content insets to be automatically
adjusted, set automaticallyAdjustsScrollViewInsets to NO. (The default
value of automaticallyAdjustsScrollViewInsets is YES.)
self.automaticallyAdjustsScrollViewInsets = NO;
i had a similar problem, after dismissing a viewController, the contentOffset from my tableView was changed to (0, -64).
my solution was a little weird, i tried all the other answers but had no success, the only thing that fixed my problem was to switch the tableView position in the controls tree of the .xib
it was the first control in the parent View like this:
I moved the tableView right after the ImageView and it worked:
it seems that putting the table view in the first position was causing the trouble, and moving the table view to another position fixed the problem.
P.D. I'm not using autoLayout neither storyboards
hope this can help someone!
it resolve my similar problem:
if ([[UIDevice currentDevice].systemVersion floatValue] >= 7){
tableView.contentInset = UIEdgeInsetsMake(-20, 0, 0, 0);
}
Try using this
tableView.separatorInset = UIEdgeInsetsZero;
Obviously, if you're supporting anything less than iOS7 you will need to ensure that the object responds to this selector before calling it.
Seriously, changing contentOffset is not the solution. You're just patching a problem and not fixing the cause. I've been facing the same problem and it turns out that grouped tableViews have a padding on the top. In my case setting the type to plain has done the trick.
Hopefully it saves someone a few minutes.
Z.
With iOS 9, none of the other answers from this page worked for me (i.e. unchecking boxes in Storyboard, setting automaticallyAdjustsScrollViewInsets to NO).
My workaround I am really dissatisfied about was this:
- (void)viewDidAppear:(BOOL)animated {
self.tableView.contentOffset = CGPointMake(0.0, 0.0);
self.tableView.contentInset = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0);
}
The same lines in viewWillAppear or viewDidLoad were ineffective.
Sometimes, I get a 64 height gap at the top of my Table View when the UIViewController is embedded inside a Navigation Controller.
In the past, I would just re-create everything, hoping that the constraints turn out correct after a clean slate.
TIL: If you don't want to make a vertical constraint to the Top Layout Guide, you can hold down the Option key to access the Container Margin.
Then make sure the Top Space to Superview constant is set to 0. This worked for me at least.
THis works for me:
- (void)loadView
{
[super loadView];
[self setAutomaticallyAdjustsScrollViewInsets:YES];
self.edgesForExtendedLayout = UIRectEdgeNone;
self.view.frame = CGRectZero;
self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
}
If you add an empty UIView before UITableView (or any view is scrollable such as ScrollView and TextView), you can have a luck.
I had a UITableViewController embedded in a container view. To get rid of the unwanted 64 points of vertical space, I needed to uncheck the 'Adjust Scroll View Insets' in Interface Builder and set the UITableView's contentInset in my UITableViewController's viewWillAppear as below.
The size of the vertical space appears to match the navigation bar's frame height and y offset. The problem only occurred on iOS 7.
- (void)viewWillAppear:(BOOL)animated;
{
[super viewWillAppear:animated];
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) {
const CGRect navBarFrame = self.navigationController.navigationBar.frame;
const CGFloat blankVerticalSpace = navBarFrame.origin.y + navBarFrame.size.height;
self.tableView.contentInset = UIEdgeInsetsMake(-blankVerticalSpace, 0, 0, 0);
}
}
In Xamarin iOS, I had this issue occuring on a backgrounded UITableViewController just after the foreground modal dialog was being dismissed. In the process of moving into the foreground, the UITableViewController had insets set (somewhere by iOS):
This class solved it
public class UITableViewControllerWithBugFix : UITableViewController {
public UITableViewControllerWithBugFix(UITableViewStyle withStyle) : base(withStyle) {
}
public override void ViewWillLayoutSubviews() {
if (TableView.ContentInset.Top != 0.0f)
TableView.ContentInset = UIEdgeInsets.Zero;
if (TableView.ScrollIndicatorInsets.Top != 0.0f)
TableView.ScrollIndicatorInsets = UIEdgeInsets.Zero;
base.ViewWillLayoutSubviews();
}
}
From time to time I get back to this awful situation, and I notice that it's still quite unknown. So, for future memory...
Lately, I'm fixing with this workaround.
Add a subview at the top of the UITableView, with zero pixel height. Works with Autolayout or without.
If I feel confident :-) I remove this fake view, fix the constraints on the top, and magically it works.
Don't ask me why, I still think it's a bug deep in UIKit.
If you go to storyboard, you can change the offset by selecting the table and under the Table View section in the Attributes inspector you just change the Separator Insets on the left to 0.
I think the real solution is to set up your top and bottom constraints on the tableview be equal to the topMargin and bottomMargin. Not the top layout guide and bottom layout guide. This allows you to keep automaticallyAdjustsScrollViewInsets to be true.
Swift 3 solution:
class PaddingLessTableView : UITableView
{
override func headerView(forSection section: Int) -> UITableViewHeaderFooterView?
{
return nil
}
override func footerView(forSection section: Int) -> UITableViewHeaderFooterView?
{
return nil
}
}
If you embedded your TableViewController in a NavigationController or a ContainerView, You have to constraint to margins instead of top Layout guide in Storyboard. Check constraint to margins when you are doing the constraints No other way worked for me. I couldn't comment so I'm just reiterating on Robert Chens answer.
I tried several of the answers. Changing the settings in storyboard caused ripple problems with an overlay menu that pops in from the left.
I only have a blank UIViewController in storyboard, otherwise everything is programmatically generated.
I have same problem with a UITableView inside a UIView inside a UIViewController. Namely, the section headers start too far down when the UIViewController is embedded in a Navigation Controller. W/o the navigation controller everything works fine.
To fix the problem I created a UILabel and with constraints placed the UILabel bottom constraint = the top constraint of the UIView (so it does not show on the screen. Now with that additional control (the new Label) the TableView behaves properly.
inputsContainerView.addSubview(titleLabel)
inputsContainerView.addSubview(tableView)
// inputsContainerView
///////////////////////////////////////
inputsContainerView.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
inputsContainerView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0).isActive = true
inputsContainerView.widthAnchor.constraint(equalTo: view.widthAnchor, constant: -40).isActive = true
inputsContainerView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.7).isActive = true
// tableView
///////////////////////////////////////
tableView.centerXAnchor.constraint(equalTo: inputsContainerView.centerXAnchor).isActive = true
tableView.topAnchor.constraint(equalTo: inputsContainerView.topAnchor).isActive = true
tableView.widthAnchor.constraint(equalTo: inputsContainerView.widthAnchor).isActive = true
tableView.heightAnchor.constraint(equalTo: inputsContainerView.heightAnchor).isActive = true
// titleLabel - inserted to stop bad section header behavior
///////////////////////////////////////
titleLabel.centerXAnchor.constraint(equalTo: inputsContainerView.centerXAnchor).isActive = true
titleLabel.bottomAnchor.constraint(equalTo: inputsContainerView.topAnchor).isActive = true
titleLabel.widthAnchor.constraint(equalTo: inputsContainerView.widthAnchor).isActive = true
titleLabel.heightAnchor.constraint(equalToConstant: 20).isActive = true
I had a same problem in iOS 11 and xib, UITableviewController and I solved it as below
[self.tableView setContentInset:UIEdgeInsetsMake(-44,0,0,0)];
If none of the above answers work, try changing the table view style to plain from grouped