I have an UITableViewController that contains a custom cell. Each cell was created using a nib and contains a single non-scrollable UITextView. I have added constraints inside each cell so that the cell adapts its height to the content of the UITextView. So initially my controller looks like this :
Now I want that when the user types something in a cell its content automatically adapts. This question has been asked many times, see in particular this or the second answer here. I have thus written the following delegate in my code :
- (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text {
[self.tableView beginUpdates];
[self.tableView endUpdates];
return YES;
}
However it leads to the following strange behavior : all constraints are ignored and all cells height collapse to the minimal value. See the picture below:
If I scroll down and up the tableView in order to force for a new call of cellForRowAtIndexPath, I recover the correct heights for the cells:
Note that I did not implement heightForRowAtIndexPath as I expect autoLayout to take care of this.
Could someone tell me what I did wrong or help me out here ? Thank you very much !
Here is a swift solution that is working fine for me. Provided you are using auto layout, you need assign a value to estimatedRowHeight and then return UITableViewAutomaticDimension for the row height. Finally do something similar to below in the text view delegate.
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
// MARK: UITextViewDelegate
func textViewDidChange(textView: UITextView) {
// Calculate if the text view will change height, then only force
// the table to update if it does. Also disable animations to
// prevent "jankiness".
let startHeight = textView.frame.size.height
let calcHeight = textView.sizeThatFits(textView.frame.size).height //iOS 8+ only
if startHeight != calcHeight {
UIView.setAnimationsEnabled(false) // Disable animations
self.tableView.beginUpdates()
self.tableView.endUpdates()
// Might need to insert additional stuff here if scrolls
// table in an unexpected way. This scrolls to the bottom
// of the table. (Though you might need something more
// complicated if editing in the middle.)
let scrollTo = self.tableView.contentSize.height - self.tableView.frame.size.height
self.tableView.setContentOffset(CGPoint(x: 0, y: scrollTo), animated: false)
UIView.setAnimationsEnabled(true) // Re-enable animations.
}
My solution is similar to #atlwx but a bit shorter. Tested with static table. UIView.setAnimationsEnabled(false) is needed to prevent cell's contents "jumping" while table updates that cell's height
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func textViewDidChange(_ textView: UITextView) {
UIView.setAnimationsEnabled(false)
textView.sizeToFit()
self.tableView.beginUpdates()
self.tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
Tested on iOS 12
I really tried a lot of solutions and finally found a good one here
This works with animation and looks beautiful. The trick was the DispatchQueue.async block.
I also used TPKeyboardAvoidingTableView to make sure the keyboard doesn't overlap anything.
func textViewDidChange(_ textView: UITextView) {
// Animated height update
DispatchQueue.main.async {
self.tableView?.beginUpdates()
self.tableView?.endUpdates()
}
}
UPDATE
I got strange jumping issues because of TPKeyboardAvoidingTableView. Especially when I scrolled to the bottom and then a UITextView got active.
So I replaced TPKeyboardAvoidingTableView by native UITableView and handle the insets myself. The table view is does the scrolling natively.
The following example works for dynamic row height as the user types text into the cell. Even if you use auto layout you still have to implement the heightForRowAtIndexPath method. For this example to work constraints must be set to textView in such a way that if cell height increases textView will also grow in height. This can be achieved by adding a top constraint and bottom constraint from textView to cell content view. But do not set height constraint for textView itself. Also enable scrolling for the textView so that textView's content size will be updated as the user enters text. Then we use this content size to calculate the new row height. As long as the row height is long enough to vertically stretch the textView to equal to or greater than its content size the text view will not scroll even if scroll is enabled and that is what you need I believe.
In this example I have only a single row and I use only a single variable to keep track of the row height. But when we have multiple rows we need a variable for each row otherwise all the rows will have the same height. An array of rowHeight that corresponds to the tableView data source array may be used in that case.
#interface ViewController ()
#property (nonatomic, assign)CGFloat rowHeight;;
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.rowHeight = 60;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell1"];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.rowHeight;
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
[self.tableView beginUpdates];
CGFloat paddingForTextView = 40; //Padding varies depending on your cell design
self.rowHeight = textView.contentSize.height + paddingForTextView;
[self.tableView endUpdates];
}
#end
Using Swift 2.2 (earlier versions would likely work too), if you set the TableView to use auto dimensions (assuming you're working in a subclassed UITableViewController, like so:
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 50 // or something
You just need to implement the delegate in this file, UITextViewDelegate, and add the below function, and it should work. Just remember to set your textView's delegate to self (so, perhaps after you've dequeued the cell, cell.myTextView.delegate = self)
func textViewDidChange(textView: UITextView) {
self.tableView.beginUpdates()
textView.frame = CGRectMake(textView.frame.minX, textView.frame.minY, textView.frame.width, textView.contentSize.height + 40)
self.tableView.endUpdates()
}
Thanks to "Jose Tomy Joseph" for inspiring (enabling, really) this answer.
I've implemented a similar approach using a UITextView however to do so I had to implement heightForRowAtIndexPath
#pragma mark - SizingCell
- (USNTextViewTableViewCell *)sizingCell
{
if (!_sizingCell)
{
_sizingCell = [[USNTextViewTableViewCell alloc] initWithFrame:CGRectMake(0.0f,
0.0f,
self.tableView.frame.size.width,
0.0f)];
}
return _sizingCell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
self.sizingCell.textView.text = self.profileUpdate.bio;
[self.sizingCell setNeedsUpdateConstraints];
[self.sizingCell updateConstraintsIfNeeded];
[self.sizingCell setNeedsLayout];
[self.sizingCell layoutIfNeeded];
CGSize cellSize = [self.sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return cellSize.height;
}
sizingCell is an instance of the cell that is only used for sizing calculations.
What's important to note is that you need to attach the UITextView's upper and lower edge to the UITableViewCells contentView's upper and lower edge so that as the UITableViewCell changes in height the UITextView also changes in height.
For constraint layout I use a PureLayout (https://github.com/smileyborg/PureLayout) so the following constraint layout code may be unusual for you:
#pragma mark - Init
- (id)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style
reuseIdentifier:reuseIdentifier];
if (self)
{
[self.contentView addSubview:self.textView];
}
return self;
}
#pragma mark - AutoLayout
- (void)updateConstraints
{
[super updateConstraints];
/*-------------*/
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeLeft
withInset:10.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop
withInset:5.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom
withInset:5.0f];
[self.textView autoSetDimension:ALDimensionWidth
toSize:200.0f];
}
Inspired by the two previous answers, I found a way to solve my problem. I think the fact that I had a UITextView was causing some troubles with autoLayout. I added the following two functions to my original code.
- (CGFloat)textViewHeightForAttributedText: (NSAttributedString*)text andWidth: (CGFloat)width {
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *font = [UIFont systemFontOfSize:14.0];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.sampleStrings[indexPath.row] attributes:attrsDictionary];
return [self textViewHeightForAttributedText:attrString andWidth:CGRectGetWidth(self.tableView.bounds)-31]+20;
}
where in the last line 31 is the sum of my constraints to the left and right sides of the cell and 20 is just some arbitrary slack.
I found this solution while reading this this very interesting answer.
The trick to immediately update the tableview cells height in a smooth way without dismissing the keyboard is to run the following snippet to be called in the textViewDidChange event after you set the size of the textView or other contents you have in the cell:
[tableView beginUpdates];
[tableView endUpdates];
However this will may not be enough. You should also make sure the tableView has enough elasticity to keep the same contentOffset. You get that elasticity by setting the tableView contentInset bottom. I suggest this elasticity value to be at least the maximum distance you need from the bottom of the last cell to the bottom of the tableView. For instance, it could be the height of the keyboard.
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0);
For more details and some useful extra features around this matter please check out the following link:
Resize and move UITableViewCell smoothly without dismissing keyboard
The solution almost everyone suggested is the way to go, I will add only a minor improvement. As a recap:
Simply set the estimated height, I do it via storyboard:
Make sure you have the constraints for the UITextView correctly set within the cell.
In the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
I simply call:
cell.myTextView.sizeToFit()
Previously beginUpdates/endUpdates were the advertised solution.
Since iOS 11, performBatchUpdates is what has been recommended source.
Calling performBatchUpdates after making a change to a cell's content works for me.
Check out the Objective C solution I have provided in the following link below.
Simple to implement, clean, and no need for auto layout. No constraints needed. Tested in iOS10 and iOS11.
Resize and move UITableViewCell smoothly without dismissing keyboard
Related
I am trying to achieve a view I mocked out on sketch. I've replicated it on Android cause I'm really good on that platform. I'm good on iOS, but the UI is kind of my weak point.
I extended a UIViewController and on my StoryBoard have the top to be a view and the bottom a tableview. The problem I'm having is centering the UITableViewCell to look like that in the app itself. Below is the solution I've tried. But, it just squeeze it all to the top. NB. I use UIView to draw those Tiny Lines in the UITableViewCell
func configureTableView() {
//First One I tried then later commented it out
loanStateTable.rowHeight = UITableViewAutomaticDimension
loanStateTable.scrollToNearestSelectedRowAtScrollPosition(UITableViewScrollPosition.Middle, animated: true)
//Second One I tried
var edgeInset = UIEdgeInsets(top: 16, left: 16, bottom: 0, right: 16)
loanStateTable.contentInset = edgeInset
}
And the storyboard view
Any help would be appreciated. Thanks
Output:
Leave everything as it is. Don't try to inset your whole TableView. Create a container View inside your TableViewCell instead:
Set the row height:
Also in code:
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return 160.0
}
Specify the right distance to the edges:
Now add the elements and specify the constraints as well:
Don't forget to set the cellIdentifier:
The result in the simulator:
If you want to look at it under the hood: I've uploaded it as github project
I don't know if I understood your question. I thought that your question was to centre vertically those two rows on screen. my code do that.
Approach:
I usually play this by adding extra cell(s) at the start and/or end of the
UITableView
#define cell_height 100
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return array.count + 1;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row > 0)
return cell_height;
else
{
CGFloat tableHeight = tableview.frame.size.height;
CGFloat contentHeight = array.count * cell_height;
CGFloat whiteAreaHeight = tableHeight - contentHeight;
if (whiteAreaHeight > 0)
return whiteAreaHeight/2.0;
else
return 0;
}
}
- (UITableViewCell *)tableView:(nonnull UITableView *)tableView cellForRowAtIndexPath:(nonnull NSIndexPath *)indexPath
{
if (indexPath.row > 0)
{
NSInteger realArrayIndex = indexPath.row - 1;
// your existing code here
return cell;
}
else
{
//return empty cell. add it to the storyboard with different identifier and get it.
}
}
I hope that helps!
From your screenshots it seems that you have a problem with the auto layout system and dynamic cell. For this is suggest to read this very good answer on stack overflow that explain how to build dynamic cells with auto layout. An important thing to consider is that creating views with auto layout is very fast and intuitive but it expensive from a performance point of view. This problem is accentuated for the UITableView where the scroll can result not smoothly. If you have simple cell (only few views and a view hierarchy with few levels) it can be ok but to the auto layout manager I suggest to:
specify a value for the property estimatedRowHeight if all the cells have the same height
implement the method tableView(_:estimatedHeightForRowAtIndexPath:) of the UITableViewDelegate if the cells have different heights.
Regarding the problem with margins I suggest to nest a UIView inside the contentView of the cell and use it as container for all the other views of the cell. In this way you can add left and right margins between this view container and the contentView so that you can center it.
Currently I have a UITableView with a resizing UITextView in it. The cell is resizing automatically using beginUpdates/endUpdates, but when it does it the table view stutters (See the gif below).
The end result is a UITableViewCell that has a textview in it that resizes based on it's content. Here is the code within the custom UITableViewCell class that causes the UITableView to update itself.
- (void)textViewDidChange:(UITextView *)textView {
// This is a category on UITableViewCell to get the [self superView] as the UITableView
UITableView *tableView = [self tableView];
if (tableView){
[tableView beginUpdates];
[tableView endUpdates];
}
}
Here are the things that I have already tried:
Get the current contentOffset and resetting it after the endUpdates but didn't work
Disabling scrolling on the UITableView before updates and then enabling afterwards
I tried returning NO always from - (BOOL)textViewShouldEndEditing:(UITextView *)textView
My UITableView cell height is using UITableViewAutomaticDimension. Any other ideas or thoughts are welcome.
Here is a sample of what it looks like:
I am not looking to use any libraries so please no suggestions for that.
Thanks
Edit: Solution
Found Here: I do not want animation in the begin updates, end updates block for uitableview?
Credit to #JeffBowen for a great find (although hacky it is workable and allows me to still implement the UITableViewDelegate methods for supporting iOS 7). Turn animations off prior to performing update and then enable after update to prevent the UITableView from stuttering.
[UIView setAnimationsEnabled:NO];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled:YES];
If you don't need to use the Delegate methods and want a less hacky solution for iOS 8+ only then go with #Massmaker's answer below.
Just disable animation before calling beginUpdates and re-enable it after calling endUpdates.
[UIView setAnimationsEnabled:NO];
[tableView beginUpdates];
[tableView endUpdates];
[UIView setAnimationsEnabled:YES];
Definitely a hack but works for now. Credit to my friend Beau who pointed me to this.
My solution (for iOS 8) was first set in my viewController viewDidLoad
self.tableView.rowHeight = UITableViewAutomaticDimension;
// this line is needed to cell`s textview change cause resize the tableview`s cell
self.tableView.estimatedRowHeight = 50.0;
then, combining this article solution in Swift and some dirty thoughts
I`ve set in my cell a property, called
#property (nonatomic, strong) UITableView *hostTableView;
and in cell-s -(void) textViewDidChange:(UITextView *)textView
CGFloat currentTextViewHeight = _textContainer.bounds.size.height;
CGFloat toConstant = ceilf([_textContainer sizeThatFits:CGSizeMake(_textContainer.frame.size.width, FLT_MAX)].height);
if (toConstant > currentTextViewHeight)
{
[_hostTableView beginUpdates];
[_hostTableView endUpdates];
}
then in viewController
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
textCell.hostTableView = self.tableView;
I don't find a way to achieve it because:
When you trigger [tableView endUpdates], table recalculate contentSize and re-set it. And this cause resetting contentOffset to default value.
This is behaviour inherited from UIScrollView, and I tried to avoid it via:
Subclassing UITableView and overriding setContentOffset: and setContentOffset:animated functions. But they don't called when table view change contentSize.
Subclassing UITableView, overriding setContentSize: function and setting contentOffset to old value, after content size updating, but it not work for me
using KVO, and setting old value for contentOffset right after it reset, but anyway I have this animated issue
setting scrollEnabled to NO and scrollToTop to NO, but it also not help
If anybody find solution for this problem welcome.
Maybe possible solution - disable autolayout: iOS: setContentSize is causing my entire UIScrollView frame to jump
UPDATE: I find solution: direct changing cell height, content size and content offset:
This works for me (table view is delegate of cell's UITextView)
- (void)textViewDidChange:(UITextView *)textView
{
CGFloat textHeight = [textView sizeThatFits:CGSizeMake(self.width, MAXFLOAT)].height;
if (self.previousTextHeight != textHeight && self.previousTextHeight > 0) {
CGFloat difference = textHeight - self.previousTextHeight;
CGRect cellFrame = self.editedCell.frame;
cellFrame.size.height += difference;
self.editedCell.frame = cellFrame;
self.contentSize = CGSizeMake(self.contentSize.width, self.contentSize.height + difference);
self.contentOffset = CGPointMake(self.contentOffset.x, self.contentOffset.y + difference);
}
self.editedNote.comments = textView.text;
}
In Textview value change delegate, use this code to resize particular cell without any flickering and all.
But before that, make sure you have used dynamic tableview cell height.
UIView.setAnimationsEnabled(false)
DispatchQueue.main.async {
self.tableView.beginUpdates()
self.tableView.endUpdates()
self.tableView.layer.removeAllAnimations()
}
UIView.setAnimationsEnabled(true)
The code I used to create a rectangle (at least until iOS7) was
CGRect rect = [cTableView frame];
rect.origin.y += [cTableView rowHeight];
searchOverlayView = [[BecomeFirstResponderControl alloc] initWithFrame:rect];
On iOS7, cTableView (an instance of a UITableView) returned 44. Testing in iOS8 with an iPhone 5s returns -1.
Why is this happening? What is the correct code that needs to be used in order for my app to be backwards compatible with iOS7?
Apple changed the default row height in iOS8 to UITableViewAutomaticDimension, which is declared as -1. This means that your table view is set up for automatic cell height calculation.
You will either need to implement autoLayout (recommended) or implement the new delegate method: heightForRowAtIndexPath. Here's a great question about auto layout: Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
Seems like you were effectively hard coding 44 (the old default) anyway, though, so you could just do that (not recommended).
This made me struggle for hours. I ended up hard coding the value to 44:
self.tableView.rowHeight = 44;
There is a performance penalty for implementing heightForRowAtIndexPath that I prefer not to incur when all rows in a table are the same height and never change at runtime (it is called once for every row, each time the table is displayed).
In this situation, I continue to set "Row Height" in the XIB and use the following iOS 8 friendly code when I need rowHeight (it works on iOS 7 and below too).
NSInteger aRowHeight = self.tableView.rowHeight;
if (-1 == aRowHeight)
{
aRowHeight = 44;
}
This allows you to keep freely editing Row Height in the XIB and will work even if Apple fixes this bug/feature in the future and a XIB set Row Height = 44 stops coming back as -1.
If you accidentally change the row height in IB from 44 to something else (like 40), automatic cell size calculation fails. You owe me 3 hours, Apple.
My solution to this problem:
#interface MCDummyTableView () <UITableViewDataSource, UITableViewDelegate>
#end
#implementation MCDummyTableView
- (instancetype) initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
frame = (CGRect){ 0, 0, 100, 100 };
self = [super initWithFrame:frame style:style];
if(!self) return self;
self.dataSource = self;
self.delegate = self;
[self registerClass:[UITableViewCell class] forCellReuseIdentifier:#"CELL"];
return self;
}
- (NSInteger) numberOfSections {
return 1;
}
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
- (UITableViewCell*) cellForRowAtIndexPath:(NSIndexPath*)indexPath {
/*
UITableView doesn't want to generate cells until it's in the view hiearchy, this fixes that.
However, if this breaks (or you don't like it) you can always add your UITableView to a UIWindow, then destroy it
(that is likely the safer solution).
*/
return [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];
}
- (UITableViewCell*) tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath {
return [self dequeueReusableCellWithIdentifier:#"CELL"];
}
- (CGFloat) defaultRowHeight {
return [self cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]].frame.size.height;
}
#end
I really don't like hardcoding things. I use this class to cache the default cell height early on in the app.
One more consideration is that if you are calculating the height based on existing view dimensions, the heightForRowAtIndexPath method may be called before viewDidLayoutSubviews.
In this case, override viewDidLayoutSubviews, and recalculate the frame.size.height value for all the visible cells.
I noticed that in iOS 7, UITableViewCells have a line break in the separator of the cell that iOS 6 does not have. Is there a way to get rid of this line break? Changing the separator to none and then making UIViews with the color of the separator still causes the white separator to occur regardless.
For iOS7:
if ([self.tableView respondsToSelector:#selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
For iOS8:
First configure your table view as follows:
if ([self.tableView respondsToSelector:#selector(layoutMargins)]) {
self.tableView.layoutMargins = UIEdgeInsetsZero;
}
Then in your cellForRowAtIndexPath: method, configure the cell as follows:
if ([cell respondsToSelector:#selector(layoutMargins)]) {
cell.layoutMargins = UIEdgeInsetsZero;
}
Note: Include both layoutMargins and separatorInset, to support both iOS versions
You can also set the 'Separator Inset' from the storyboard:
If you want to support older versions of iOS, you should check for the availability of this method before calling it:
if ([self.tableView respondsToSelector:#selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:UIEdgeInsetsZero];
}
Figured it out.
[self.tableView setSeparatorInset:UIEdgeInsetsMake(0, 0, 0, 0)];
The selected answer did not work for me. But this yes and it works from ios 2.0:
[self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
Swift version
iOS introduces the layoutMargins property on cells AND table views.
This property isn't available in iOS 7.0 so you need to make sure you check before assigning it!
However, Apple has added a **property to your cell that will prevent it from inheriting your Table View's margin settings. This way, your cells can configure their own margins independently of the table view. Think of it as an override.
This property is called preservesSuperviewLayoutMargins, and setting it to NO can allow you to override your Table View's layoutMargin settings with your own cell's layoutMargin setting. It both saves time (you don't have to modify the Table View's settings), and is more concise. Please refer to Mike Abdullah's answer for a detailed explanation.
NOTE: this is the proper, less messy implementation, as expressed in Mike Abdullah's answer; setting your cell's preservesSuperviewLayoutMargins=NO will ensure that your Table View does not override the cell settings.
First step - Setup your cell margins:
/*
Tells the delegate the table view is about to draw a cell for a particular row.
*/
override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath)
{
// Remove seperator inset
if cell.respondsToSelector("setSeparatorInset:") {
cell.separatorInset = UIEdgeInsetsZero
}
// Prevent the cell from inheriting the Table View's margin settings
if cell.respondsToSelector("setPreservesSuperviewLayoutMargins:") {
cell.preservesSuperviewLayoutMargins = false
}
// Explictly set your cell's layout margins
if cell.respondsToSelector("setLayoutMargins:") {
cell.layoutMargins = UIEdgeInsetsZero
}
}
Setting the preservesSuperviewLayoutMargins property on your cell to NO should prevent your table view from overriding your cell margins. In some cases, it seems to not function properly.
Second step - Only if all fails, you may brute-force your Table View margins:
/*
Called to notify the view controller that its view has just laid out its subviews.
*/
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Force your tableview margins (this may be a bad idea)
if self.tableView.respondsToSelector("setSeparatorInset:") {
self.tableView.separatorInset = UIEdgeInsetsZero
}
if self.tableView.respondsToSelector("setLayoutMargins:") {
self.tableView.layoutMargins = UIEdgeInsetsZero
}
}
...and there you go! This should work on iOS 8 as well as iOS 7.
Note: tested using iOS 8.1 and 7.1, in my case I only needed to use The First Step of this explanation.
The Second Step is only required if you have unpopulated cell beneath the rendered cells, ie. if the table is larger than the number of rows in the table model. Not doing the second step would result in different separator offsets.
For a permanent solution app-wide, call this in application:didFinishLaunching .. any tableview will be "fixed"
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 7) {
[[UITableView appearance]setSeparatorInset:UIEdgeInsetsZero];
}
And in iOS 8, based on the Seth McFarlane post here, you need to start dealing with layoutMargins. The following did it for me:
In ApplicationDidFinishLaunching, run the following:
if ([UITableViewCell instancesRespondToSelector:#selector(setLayoutMargins:)]) {
[[UITableViewCell appearance]setLayoutMargins:UIEdgeInsetsZero];
}
Create the following category on UITableView:
#interface UITableView(WJAdditions)
-(void) wjZeroSeparatorInset;
#end
#implementation UITableView (WJAdditions)
-(void) wjZeroSeparatorInset {
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000
if ([self respondsToSelector:#selector(setLayoutMargins:)]) {
self.layoutMargins = UIEdgeInsetsZero;
}
#endif
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000
if ([self respondsToSelector: #selector(setSeparatorInset:)])
self.separatorInset = UIEdgeInsetsZero;
}
#endif
}
#end
In your UITableViews, shortly after initializing, call
[self wjZeroSeparatorInset];
In your UITableViewControllers, you need the following:
-(void) viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
UITableView* tv = self.tableView;
if ([tv respondsToSelector:#selector(setLayoutMargins:)]) {
[tv setLayoutMargins:UIEdgeInsetsZero];
}
}
In iOS 8, there appears to be a small bug. The custom value for separator inset doesn't get applied to the cells with text in it. I tried setting it from the interface builder and through code but neither worked. I've filed a bug report as well.
In the meantime, I have a small workaround to achieve this. I simply draw the line on the cell. Create a subclass of UITableViewCell and override the drawRect method. Also don't forget to set the separator property of the UITableView to None. Here is the code. It's in Swift but since its basically C, you can use the same thing in Objective-C as well.
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let context = UIGraphicsGetCurrentContext()
CGContextSetStrokeColorWithColor(context, UITableView().separatorColor.CGColor) // seperator color
CGContextSetLineWidth(context, 2) // separator width
CGContextMoveToPoint(context, 0, self.bounds.size.height)
CGContextAddLineToPoint(context, self.bounds.size.width, self.bounds.size.height)
CGContextStrokePath(context)
}
Reset separatorInset fixes this problem for me:
if ([self.tableView respondsToSelector:#selector(setSeparatorInset:)]) {
[self.tableView setSeparatorInset:self.tableView.separatorInset];
}
The solution:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if ([tableView respondsToSelector:#selector(setSeparatorInset:)]) {
[tableView setSeparatorInset:UIEdgeInsetsZero];
}
if ([tableView respondsToSelector:#selector(setLayoutMargins:)]) {
[tableView setLayoutMargins:UIEdgeInsetsZero];
}
if ([cell respondsToSelector:#selector(setLayoutMargins:)]) {
[cell setLayoutMargins:UIEdgeInsetsZero];
}
}
Beware as setting the separatorInset of tableView to UIEdgeInsetsZero seems to break the left margin of section titles at the same time! (at least it does on iOS 7.1)
If you want to display section titles with tableView:titleForHeaderInSection: and still get the desire effect of no line break between UITableViewCells, you must set the separatorInset directly on the cells instead of the tableView!
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// ...
[cell setSeparatorInset:UIEdgeInsetsZero];
For iOS8+ this worked for me,
tableView.layoutMargins = UIEdgeInsetsZero
and in cellForRowAtIndexPath method:
cell.layoutMargins = UIEdgeInsetsZero;
- (void)viewDidLoad
{
[self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
[super viewDidLoad];
// Additional setup
}
Note: the setSeparatorStyle line has to be above the super call.
#implementation UITableView (HGApperance)
-(void)setSeparatorXMargin:(CGFloat)margin{
// iOS 7
if ([self respondsToSelector:#selector(setSeparatorInset:)]) {
[self setSeparatorInset:UIEdgeInsetsZero];
}
if ([self respondsToSelector:#selector(layoutMargins)]) {
self.layoutMargins = UIEdgeInsetsZero;
}
}
#end
For TableviewCell, Willam's answer works perfect.
To iOS8 and above
If you find that all the solutions don't work like me, I think you may have used the subclass of UITableViewCell and override the layoutSubviews method, if you meet two the conditions, go on reading.
Do or check it step by step.
1、After alloc and init the UITableView, set its layoutMargins property.
if ([_tableView respondsToSelector:#selector(setLayoutMargins:)]) {
_tableView.layoutMargins = UIEdgeInsetsZero ;
}
2、In the method - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath, after alloc and init your custom UITableViewCell, set its layoutMargins property.
if ([cell respondsToSelector:#selector(setLayoutMargins:)]) {
cell.layoutMargins = UIEdgeInsetsZero ;
}
3、The most important part comes in. If you override the - (void)layoutSubviews method of UITableViewCell, don't forget to call its super.
- (void)layoutSubviews
{
[super layoutSubviews] ;
// layout the subviews
}
Xamarin.iOS Version
I extended the UITableView separator the full width in Xamarin.iOS on iOS 8.4 with help from posts by Luis E. Prado and King-Wizard. I had to set both SeparatorInset and LayoutMargins to get it working. I did not write checks for lower iOS versions since I am targeting iOS 8 and above.
I wrote the following two C# extension methods to set the UITableView separator full width at the UITableView or UITableViewCell level. You might use one or both of these methods depending on the desired effect (see the explanations below).
UITableView
Set the UITableView properties to alter the separators that appear after empty cells. This SetLayoutMarginsZero method could be called from the related ViewController's ViewDidLoad method.
public static void SetLayoutMarginsZero(this UITableView tableView)
{
tableView.SeparatorInset = UIEdgeInsets.Zero;
tableView.LayoutMargins = UIEdgeInsets.Zero;
}
UITableViewCell
Set the UITableViewCell properties to alter the separators that appear after populated cells. This SetLayoutMarginsZero method could be called from the TableViewSource GetCell method.
public static void SetLayoutMarginsZero(this UITableViewCell tableViewCell)
{
tableViewCell.SeparatorInset = UIEdgeInsets.Zero;
tableViewCell.LayoutMargins = UIEdgeInsets.Zero;
tableViewCell.PreservesSuperviewLayoutMargins = false;
}
SUMMARY
Given that we don't always know what the frame of a cell or its content view is going to be (due to editing, rotation, accessory views etc.), what is the best way to calculate the height in tableView:heightForRowAtIndexPath: when the cell contains a variable height text field or label?
One of my UITableViewController's contains the following presentation:
UITableViewCell with UITextView.
UITextView should be the same width and height as UITableViewCell.
I created the UITableViewCell subclass, and then and initialized it with UITextView (UITextView is a private field of my UITableViewController)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"TextViewCell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[BTExpandableTextViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier textView:_notesTextView] autorelease];
}
return cell;
}
I implemented the following method in my UITableViewCell subclass:
- (void)layoutSubviews{
[super layoutSubviews];
CGFloat height = [textView.text sizeWithFont:textView.font constrainedToSize:CGSizeMake(textView.frame.size.width, MAXFLOAT)].height + textView.font.lineHeight;
textView.frame = CGRectMake(0, 0, self.contentView.frame.size.width, (height < textView.font.lineHeight * 4) ? textView.font.lineHeight * 4 : height);
[self.contentView addSubview:textView];
}
and of course i implemented the following UITableViewDataSource method (look! I am using self.view.frame.size.width (but really i need UITableViewCell contentView frame width):
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
CGFloat height = [_notesTextView.text sizeWithFont:_notesTextView.font
constrainedToSize:CGSizeMake(self.view.frame.size.width, MAXFLOAT)].height;
CGFloat groupedCellCap = 20.0;
height += groupedCellCap;
if(height < [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font]){
height = [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font];
}
return height;
}
also I implemented the following method (thats not so important but ill post it anyway, just to explain that cell's height is dynamical, it will shrink or expand after changing text in UITextView)
- (void)textViewDidChange:(UITextView *)textView{
CGFloat height = [_notesTextView.text sizeWithFont:_notesTextView.font
constrainedToSize:CGSizeMake(_notesTextView.frame.size.width, MAXFLOAT)].height;
if(height > _notesTextView.frame.size.height){
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
}
And now, my question is:
After loading view, UITableViewController is calling methods in the following order: (ill remove some, like titleForHeaderInSection and etc for simplification)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
and only then
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Look! I should return the correct UITableViewCell height before cellForRowAtIndexPath!
That means: I don't know UITableViewCell contentView frame. And i can't get it programmatically.
This width can be one of:
iPhone plain table, portrait orientation
iPhone plain table, landscape orientation
iPhone grouped table, portrait orientation
iPhone grouped table, landscape orientation
and the same for the iPad ( another 4 values )
And don't forget that contentView frame can be smaller because of UITableViewCell accessoryType, or because of UITableView editing state. (for example if we have UITableViewCell with multiline UILabel of any height in any editing state and with any accessoryView)
So this problem is fundamental: I just can't get cell contentView frame width for constraining, because I should return this height before cell layouts contentView. (And this is pretty logical, by the way) But this contentView frame really matters.
Of course sometimes I can know this width exactly and "hardcode" it
(for example: UITableViewCellAccessoryDisclosureIndicator has 20 px width, and tableView cannot be in editing state, then I can write self.view.frame.size.width - 20 and the task is done)!
Or sometimes contentView is equal to UITableViewController's view frame!
Sometimes I'm using self.view.frame.width in -tableView:heightForRowAtIndexPath: method.. (like now, and it works pretty well, but not perfectly because of grouped UITableView, should subtract some constant values, and they are different for 2 devices * 2 orientations)
Sometimes I have some #defined constants in UITableViewCell (if I know width exactly)...
Sometimes I'm using some dummy pre-allocated UITableViewCell (what is just stupid, but sometimes is pretty elegant and easy for use)...
But I don't like anything of that.
What's the best decision?
Maybe i should create some helper class, that will be initialized with such parameters:
accessory views, device orientation, device type, table view editing state, table view style (plain, grouped), controller view frame, and some other, that will include some constants (like grouped tableView offset, etc) and use it to find the expected UITableViewCell contentView width? ;)
Thanks
Table view uses the tableView:heightForRowAtIndexPath: method to determine its contentSize before creating any UITableViewCellcells. If you stop and think about it, this makes sense, as the very first thing you would do with a UIScrollView is set its contentSize. I have run into a similar problem before, and what I've found is that it is best to have a helper function that can take the content going into the UITableViewCell and predict the height of that UITableViewCell. So I think you will want to create some sort of data structure that stores the text in each UITableViewCell, an NSDictionary with NSIndexPaths as keys and the text as values would do nicely. That way, you can find the height of the text needed without referencing the UITableViewCell.
Although you can calculate heights for labels contained in table view cells, truly dynamically, in '- layoutSubviews' of a UITableViewCell subclass, there's no similar way of doing this (that I know of) for cell heights in '- tableView:heightForRowAtIndexPath:' of a table view delegate.
Consider this:
- (void)layoutSubviews
{
[super layoutSubviews];
CGSize size = [self.textLabel.text sizeWithFont:self.textLabel.font
constrainedToSize:CGSizeMake(self.textLabel.$width, CGFLOAT_MAX)
lineBreakMode:self.textLabel.lineBreakMode];
self.textLabel.$height = size.height;
}
Unfortunately though, by the time '- tableView:heightForRowAtIndexPath:' is called, that is too early, because cell.textLabel.frame is yet set to CGRectZero = {{0, 0}, {0, 0}}.
AFAIK you won't be able to do this neither with content view's frame, nor summing up individual labels' frames...
The only way I can think of is to come up with a convenience class, methods, constants, or such that will try to cover up all possible width in any device orientation, on any device:
#interface UITableView (Additions)
#property (nonatomic, readonly) CGFloat padding;
#end
#implementation UITableView (Additions)
- (CGFloat)padding
{
if (self.formStyle == PTFormViewStylePlain) {
return 0;
}
if (self.$width < 20.0) {
return self.$width - 10.0;
}
if (self.$width < 400.0 || [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
return 10.0;
}
return MAX(31.0, MIN(45.0, self.$width * 0.06));
}
#end
Also note that, recently we also have new iPhone 5's 4-inch width (568 instead of 480) in landscape orientation.
This whole thing is pretty disturbing, I know... Cheers.