I'm creating a view which comprises of a UITableView with customised cells - I'm overriding drawRect in my custom view. I've tried overriding UITableViewCell and adding my custom view as an IBOutlet, I've tried not overriding it and just referring to it by [[cell subviews] objectAtIndex:0]; both yield the same results.
When I first look at the view, all is fine. If I scroll slowly, all is fine. As soon as I scroll quickly the reused cells are clearly not re-drawing because I end up with the custom drawing, being wrong for the particular cells.
The cell configuration method...
- (UITableViewCell*) tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"DialogCell"];
MaskedRoundedCornerDIalogCell* dialogCell = (MaskedRoundedCornerDIalogCell*)[[[cell contentView] subviews] objectAtIndex:0];
[dialogCell setPadding:10];
if (indexPath.row % 2 == 0) {
[dialogCell setAlignLeft:YES];
[dialogCell setMaskTopLeftOnly];
[[dialogCell textContent] setText:#"LEFT ALIGNED TEXT"];
[[dialogCell textContent] setTextAlignment:NSTextAlignmentLeft];
} else {
[dialogCell setAlignLeft:NO];
[dialogCell setMaskBottomRightOnly];
[[dialogCell textContent] setText:#"RIGHT ALIGNED TEXT"];
[[dialogCell textContent] setTextAlignment:NSTextAlignmentRight];
}
return cell;
}
My Custom drawing code in my MaskedRoundedCornerDIalogCell implementation (A class which extends UIView and is added to the UITableViewCell):
- (void) drawRect:(CGRect)rect {
int maxWidth = rect.size.width - 50;
CGRect container;
if (_alignLeft) {
container = CGRectMake(rect.origin.x + _padding, rect.origin.y + _padding, maxWidth - (2* _padding), rect.size.height - (2*_padding));
} else {
container = CGRectMake((rect.size.width - _padding) - (maxWidth - (2* _padding)), rect.origin.y + _padding, maxWidth - (2* _padding), rect.size.height - (2*_padding));
}
UIRectCorner roundedCorners;
if (!_maskTopLeft) {
roundedCorners = roundedCorners | UIRectCornerTopLeft;
}
if (!_maskTopRight) {
roundedCorners = roundedCorners | UIRectCornerTopRight;
}
if (!_maskBottomLeft) {
roundedCorners = roundedCorners | UIRectCornerBottomLeft;
}
if (!_maskBottomRight) {
roundedCorners = roundedCorners | UIRectCornerBottomRight;
}
UIBezierPath* containerBezierPath = [UIBezierPath bezierPathWithRoundedRect:container byRoundingCorners: roundedCorners cornerRadii:CGSizeMake(25.0F, 25.0F)];
[[UIColor lightGrayColor] setFill];
[containerBezierPath fillWithBlendMode: kCGBlendModeNormal alpha:1.0f];
}
The way it looks when I first launch it:
The way it looks after scrolling a few times:
Any advice, gratefully received...
I'm not sure if you're overriding drawRect in your UITableViewCell subclass or if it's in a custom UIView class. My suggestion would be to do your drawing in a custom UIView class and then add that view as a subview of your cell - just in case UITableViewCell is doing something in drawRect that you're accidentally overriding.
In any case, the reason you're seeing this behaviour is because drawRect is only called when the view first comes on the screen or if it's invalidated. From the docs:
This method is called when a view is first displayed or when an event occurs that invalidates a visible part of the view. You should never call this method directly yourself. To invalidate part of your view, and thus cause that portion to be redrawn, call the setNeedsDisplay or setNeedsDisplayInRect: method instead.
In your cell's setMask.. methods, try calling [self.customDrawingView setNeedsDisplay] to invalidate the drawing and force an update.
You can use this method or the setNeedsDisplayInRect: to notify the system that your view’s contents need to be redrawn. This method makes a note of the request and returns immediately. The view is not actually redrawn until the next drawing cycle, at which point all invalidated views are updated.
You should use this method to request that a view be redrawn only when the content or appearance of the view change. If you simply change the geometry of the view, the view is typically not redrawn. Instead, its existing content is adjusted based on the value in the view’s contentMode property. Redisplaying the existing content improves performance by avoiding the need to redraw content that has not changed.
At first, I don't think, that overriding - (void)drawRect:(CGRect)rect is the right resolution for your problem. My advice would be to override it only if you exactly know what are you doing and what are you trying to achieve.
According to images, you have provided, the problem you are facing is related with UITableViewCell reusing, but not with the drawing.
The problem. When table view scrolling hits the boundary of UITableView to present new content, UITableView doesn't create a new cell instance. It takes the currently invisible cell (the one which has been to scrolled out to present new one on screen) and reuses it (UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"DialogCell"];). In your example, lines of code executed after the cell is being reused only sets the cell content text up. From the images you provided, I see, that the text is sat up correctly. The problem is with cells background. Since there is not a single line of code to setup cells background after it is being reused, background before reuse is being presented.
Recommendations. Obviously, you need to override cells background each time it is being reused. If I were you, I would create a subclass of class UITableViewCell, lets say MyTableViewCell. The cell would have a method like:
- (void)setupWithSide:(Side)side {
if (side == SideRight) {
[self setupRightAlignedCintent];
[self setupLeftBackground];
}
}
Meanwhile, in UITableViewController you have to
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableViewCell *cell = (MyTableViewCell *)[tableView dequeueReusableCellWithIdentifier:#"DialogCell"];
Side side;
if (indexPath.row % 2 == 0) {
side = SideRight;
} else {
side = SideLeft;
}
[cell setupWithSide:side];
return cell;
}
Related
I am learning about UITableview on iOS and following a course online. I get the table showing fine, but the images on my cells are not all the way to the left (whereas the instructor's ones are). Here is a screenshot of the cells in question:
I don't want that gap, I want the images to be positioned right at the beggining of the cell, all the way to the left. I have done some research and it seems Apple has changed the default look of the cells between ios6 and ios7 so that now the images in cells show a little gap at the left. To get rid of it, I have tried UIEdgeInsets:
[tableView setSeparatorInset:UIEdgeInsetsZero];
and that's not working. I also have tried this approach:
cell.imageView.frame = CGRectMake( 0, 0, 50, 55 );
Nothing happens. So how would I go about it? Thanks
edit-------------------------------------------------------------------------------------------------------------------------------
Still not have found the answer to this. The solutions posted here don't work. I found this piece of code:
self.tableView.contentInset = UIEdgeInsetsMake(0, -50, 0, 0);
Which besides completely puzzling me (as the parameter affected should be the y?) I thought solved the issue by making the image on the cell appear all the way to the left, until I realised it only moved the whole view to the left (as I should have expected I guess) leaving an equal gap on the other side of the screen. All I want is for my images in the cells to appear all the way to the left of the cell as it used to be the case on previous ios. Thanks
It happens because default table content offset from left is 15, you should change it with 0.
See this once, you get idea Remove empty space before cells in UITableView
If you create custom cells. UITableViewCell have owner imageView. Change title of image in your cell.
If you use default cell, use custom cell with constraint Leading space = 0.
It is better not use default imageView of the cell. Drag and drop UIImageView from objective library, create a custom table view cell (Child class of UITableViewCell) then create and outlet of the image view just dragged.
The spacing in the UITableViewCell is because of the default TRUE returned by shouldIndentWhileEditingRowAtIndexPath method of UITableViewDelegate.
I was able to reproduce your problem by the below scenario:
UITableView is in editable mode:
self.tableView.editing = true
And you have implemented:
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView
editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewCellEditingStyleNone;
}
To correct your code:
If you do not want to set Editing Style then you can turn off the editing mode by
self.tableView.editing = false
and remove editingStyleForRowAtIndexPath.
Else if you need editing mode then set the appropiate Editing style(UITableViewCellEditingStyleDeleteor UITableViewCellEditingStyleInsert) or simply turn the indentation off.
- (BOOL)tableView:(UITableView *)tableView
shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath {
return FALSE;
}
You must create a custom cell, by adding a new class as a subclass of UITableViewCell. then you can design cell with autolayout and constraints which will resolve the issue.
there is a another concrete way to achieve this by creating subclass uitableviewcell (custom class).
steps to follow
create a class subclass of UITableViewCell.
in .h file create properties and outlets of UI components.
go to storyboard and add table view cell inside the tableview.
now add UI components like: imageview or button etc and set the x, y values according to.
make class of custom cell your className using identity inspector see image.
connect all outlets of UI components.
use below code uitableview
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *MyIdentifier = #"uniqueIdentifire";
yourCustomClassForCell *cell = (yourCustomClassForCell *)[tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil){
cell = [[yourCustomClassForCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier];
}
cell.imageView.image = [imageAry objectAtIndex:indexPath.row];
}
Dont forget to give identifire by selecting your cell using storyboard Attribute inspector uniqueIdentifire to identifire property see image.
Also you can give some vertical space between cells by just to add this below code (Method only) inside customeCellClass.
- (void)setFrame:(CGRect)frame { // method to insert gap between table view cell
frame.origin.y += 6;
frame.size.height -= 2 * 6;
[super setFrame:frame];
}
You can not really change the frame of the inbuilt subviews of uitableviewcell like imageview, accessoryview. But if you create a custom tableviewcell class(even if you do not add any other subelement to it), you can change the frame of the inbuilt imageview by overriding the layoutSubviews method inside the UITableViewCell. I have tried it and it works.
#import "TableViewCell.h"
#implementation TableViewCell
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
-(void) layoutSubviews{
[super layoutSubviews];
CGRect frame = self.imageView.frame;
frame.origin.x = 0;
self.imageView.frame = frame;
}
#end
I have UITableViewCell that contains UIView (lets call it CPView) which is created while cellForRowAtIndexPath is called. CPView is just a plain coloured view and for every cell its width is different (that's why needed to create in cellForRowAtIndexPath).
Problem is
1)The CPView 's colour gets darker every time cell loads (May be due to every time that cell creates the same view so overlapping effect).
2) The cell overlaps / inherits other cell's CPView (we can see this because of light and dark colour of two CPView).
How can I prevent cell to recreate if it already exist or creation of this CPView again?
Edit
- (void)configureCell:(CreditDebitCell *)cell atIndexPath:(NSIndexPath *)indexPath {
//other code
UIView * CPView;
if (CPView){
CPView =nil;
}
else
{
CPView = [[UIView alloc] initWithFrame:CGRectMake(cell.bounds.origin.x, cell.bounds.origin.y, cell.frame.size.width*[self.percentArray[indexPath.row] floatValue] ,cell.frame.size.height )];
[CPView setClipsToBounds:YES];
[CPView setBackgroundColor:[UIColor colorWithRed:107/255.0 green:15/255.0 blue:47/255.0 alpha:0.5]];
[cell addSubview: CPView];
}
}
The issue here is reuse of the cells - and therefore you get multiple views added to your cell view.
You can:
-remove subview
-check if subview exists and do/don't do anything.
You can check if the subview is there by going through subviews:
for (UIView *v in cell.contentView.subview) {
if ([v isKindOfClass:[CPView class]]) {
// remove or flag that it exists
}
}
But I think that you should handle this in your cell - not your view controller that implements table view delegate. Better tell cell to use some view/hide some view based on some kind of logic then to do that inside cellForRowAtIndexPath
According to your i question(without cellforRowAtIndexpath) i can assume that you should check every time something like in cellForRowAtIndexPath
if(cpView){
cpView = nil;
}
// alloc again with required size for particular row.
Make a subclass of your UITableViewCell and make a property of it that will reference your CPView. This will now let you have a better control whether your subclassed cell does / doesn't have any CPView that needs to be added.
I have a UITableViewController with some custom cells and an UIView within each cell. I have implemented the controller's cellForRowAtIndexPath to configure the UIView in each cell. For this purpose I need to know the width of the UIView on screen. Since I am using autolayout and size classes to automatically change the size of the UIView based on device orientation, I have implemented an additional method of getting the width runtime.
The problem is that when the table view is presented the first time, my code reports width for UIVIew from a compact width size class even when I am using the device in the landscape orientation. The system renders all the views as should, but my code to get the width is not working. Scrolling new cells visible or an orientation change will remedy the situation immediately.
My code:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ...
CustomCell* cell = [tableView dequeueReusableCellWithIdentifier:#"CustomCell"];
if (cell == nil) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"CustomCell" owner:self options:nil];
cell = [nib firstObject];
} else {
// Clear custom content
NSArray *viewsToRemove = [cell.histogramView subviews];
for (UIView *v in viewsToRemove) {
[v removeFromSuperview];
}
}
[cell setNeedsLayout];
[cell layoutIfNeeded];
int width = ((CustomView*)cell.customView).getWidth;
NSLog(#"width = %d", width);
// ...
}
And then:
#implementation CustomView
- (int)getWidth {
[self setNeedsLayout];
[self layoutIfNeeded];
int width = self.frame.size.width;
return width;
}
#end
Edited to add:
The problem seems to be that at when cellForRowAtIndexPath is called the first time tableview appears, autolayout has not occurred for the cell. Forcing it with [cell setNeedsLayout] and [cell layoutIfNeeded] right after creating the cell does not do the trick either.
It seems my problem root cause is a potential duplicate of How to know the width of an UITableViewCell when using auto layout? So the problem has to do with fact that when my CustomCell is loaded from a nib, it will have the default frame. Special tricks should be done to force autolayout. However, the accepted answer does not work for cells that are initially out of visible area. Any takers on this?
I would suggest trying to use constraints and ratios instead of actual width of the frame, since, as stated by Marcus Adams, the frame will have the value you are actually looking for only after viewDidAppear.
For example if your cells contain a UILabel that is 1/3 the width of the cell, and a UIImageView that fills the space left, you can set constraints between them and the parent view.
Can your logic be changed like this?
Another suggestion would be to call reloadData on your table view after viewDidAppear, but that is definitely not a nice option UX-wise.
I found out the correct solution to this:
Create a custom cell like in the original question.
Set cell.contentMode = UIViewContentModeRedraw in tableview: cellForRowAtIndexPath:
Implement layoutSubviews for the custom cell like so:
- (void)layoutSubviews {
[super layoutSubviews];
NSLog(#"cell width == %f", self.bounds.size.width);
}
This way you will have access to the size of the cell as it will appear on screen.
I have the a custom tableview cell with applied constraints but the first time the table is displayed the row height is not resized properly unless new cells are created, is there a way to do this without calling reloadData again?
Yes. This is actually an issue with self-sizing that you need to work around until it is fixed.
The problem is that when a cell is instantiated, its initial width is based on the storyboard width. Since this is different from the tableView width, the initial layout incorrectly determines how many lines the content actually would require.
This is why the content isn't sized properly the first time, but appears correctly once you (reload the data, or) scroll the cell off-screen, then on-screen.
You can work around this by ensuring the cell's width matches the tableView width. Your initial layout will then be correct, eliminating the need to reload the tableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[cell adjustSizeToMatchWidth:CGRectGetWidth(self.tableView.frame)];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
In TableViewCell.m:
- (void)adjustSizeToMatchWidth:(CGFloat)width
{
// Workaround for visible cells not laid out properly since their layout was
// based on a different (initial) width from the tableView.
CGRect rect = self.frame;
rect.size.width = width;
self.frame = rect;
// Workaround for initial cell height less than auto layout required height.
rect = self.contentView.bounds;
rect.size.height = 99999.0;
rect.size.width = 99999.0;
self.contentView.bounds = rect;
}
I'd also recommend checking out smileyborg's excellent answer about self-sizing cells, along with his sample code. It's what tipped me off to the solution, when I bumped into the same issue you are having.
Update:
configureCell:forRowAtIndexPath: is an approach Apple uses in its sample code. When you have more than one tableViewController, it is common to subclass it, and break out the controller-specific cellForRowAtIndexPath: code within each view controller. The superclass handles the common code (such as dequeuing cells) then calls the subclass so it can configure the cell's views (which would vary from controller to controller). If you're not using subclassing, just replace that line with the specific code to set your cell's (custom) properties:
cell.textLabel.text = ...;
cell.detailTextLabel.text = ...;
I don't really know how to explain what I want, so here's an picture :
I have a view with a lot of subviews (gray lines). Then the background (blue) is a picture (UIImageView + Blur effect), so I need it to stay and not to scroll. Behind the background, there's a view (orange). I want the picture (blue) to scroll only when the subviews (gray) are at the bottom (3rd picture).
Should I use embed scrollviews, or can I get this effect with only one UIScrollView ? If multiple scrollviews, does someone have an example ?
Thanks a lot for your help
Use only one scroll view, and use its delegate method 'scrollViewDidScroll' to read the content offset. Use this value to translate your background image.
It's not necessary to use 2 scroll views. But I would use it since I prefer setting contentOffset of the background scroll view than setting it's frame directly.
The idea is that you implement scrollViewDidScroll: delegate method for the front scroll view. And track the contentOffset to check whether the scrolling has reached the end of it's content by checking whether scrollView.contentOffset.y + scrollView.bounds.size.height > scrollView.contentSize.height or not.
If it has, then you offset the location of the background view with the amount of offset that exceed the content size.
Please see the code below. You can skip all the code and only look at scrollViewDidScroll:.
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.backgroundScrollView.userInteractionEnabled = NO;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 20;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
cell.textLabel.text = [NSString stringWithFormat:#"Item %ld", (long)indexPath.row];
return cell;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat backgroundVerticalOffset = MAX(scrollView.contentOffset.y + scrollView.bounds.size.height - scrollView.contentSize.height, 0);
CGPoint backgroundContentOffset = CGPointMake(0.0, backgroundVerticalOffset);
self.backgroundScrollView.contentOffset = backgroundContentOffset;
// If you decide not to use two scroll views, then please use backgroundContentOffset to set the backgroundView.frame.origin.y instead
// e.g. backgroundView.frame.origin.y = -backgroundContentOffset;
}
The result shown below:
P.S. I used blue background view instead of an image view and everything else with clear background color.