By default, a UITableViewCell instance positions the label pair textLabel/detailTextLabel in the center of its parent view. I prefer to have the pair aligned to the top of the cell. How do I do that programmatically?
Thanks,
Doug
The docs describe -[UIView layoutSubviews] as
"Overridden by subclasses to layout subviews when layoutIfNeeded is invoked. The default implementation of this method does nothing."
That description is not elaborate, but is accurate. In this case, the method's behavior is to layout your subviews. It will be invoked anytime the device orientation changes. It is also scheduled for later invocation whenever you call -setNeedsLayout.
Since, UIView's implementation does nothing, (and I presume the same for UIControl), you get total creative freedom to make your UIView subclass subviews be positioned wherever you want them.
In subclassing a UITableViewCell, you have a couple of options to try:
Override -layoutSubviews and
manipulate the position of the built-in textLabel and -detailLabel views.
Override -viewDidLoad,
create two of your own UILabels to provide the text and detailed text,
add them to self.contentView, and
override -layoutSubviews to manipulate the position of your custom UILabel views
In a related SO question, the recommendation is to avoid #1, manipulating the built-in textLabel and detailTextLabel.
A more reliable bet would be to do something like this:
#interface MyTableViewCell : UITableViewCell {
UILabel *myTextLabel;
UILabel *myDetailTextLabel;
}
// ... property declarations
#end
#implementation MyTableViewCell
#synthesize myTextLabel, myDetailTextLabel;
- (id) initWithFrame: (CGRect)frame {
self = [super initWithFrame: frame];
if (self) {
myTextLabel = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease];
[self.contentView addSubview: myTextLabel];
myDetailTextLabel = [[[UILabel alloc] initWithFrame:CGRectZero] autorelease];
[self.contentView addSubview: myDetailTextLabel];
}
return self;
}
- (void) dealloc {
[myTextLabel release];
[myDetailTextLabel release];
[super dealloc];
}
- (void) layoutSubviews {
// Let the super class UITableViewCell do whatever layout it wants.
// Just don't use the built-in textLabel or detailTextLabel properties
[super layoutSubviews];
// Now do the layout of your custom views
// Let the labels size themselves to accommodate their text
[myTextLabel sizeToFit];
[myDetailTextLabel sizeToFit];
// Position the labels at the top of the table cell
CGRect newFrame = myTextLabel.frame;
newFrame.origin.x = CGRectGetMinX (self.contentView.bounds);
newFrame.origin.y = CGRectGetMinY (self.contentView.bounds);
[myTextLabel setFrame: newFrame];
// Put the detail text label immediately to the right
// w/10 pixel gap between them
newFrame = myDetailTextLabel.frame;
newFrame.origin.x = CGRectGetMaxX (myTextLabel.frame) + 10.;
newFrame.origin.y = CGRectGetMinY (self.contentView.bounds);
[myDetailTextLabel setFrame: newFrame];
}
#end
In MyTableViewCell, you would ignore the built-in text labels and use your own custom UILabels. You take complete control over positioning them within the content rect of the table view cell.
I'm leaving a lot of stuff out. In doing custom layout with text labels, you'd want to consider:
Figuring out your own layout algorithm.
I'm using a layout algorithm above that resizes the custom UILabels to fit their text content, then positions them side-by-side. You'll likely want something more specific to your app.
Keeping the custom labels within the content view.
In -layoutSubviews, you might want logic to keep the custom UILabels sized and positioned so that they don't fall outside the bounds of the content rect. With my naive layout logic, any long text dropped into either UILabel (or both) could cause the labels to be positioned right out of the content view bounds.
How to handle -viewDidLoad/-viewDidUnload.
As coded above, this subclass doesn't handle being loaded from a nib. You might want to use IB to layout your cell, and if you do, you'll need to think about -viewDidLoad/-viewDidUnload/-initWithCoder:
The following method in your UITableViewCell subclass should quickly and concisely align both textLabel and detailTextLabel to the top of the cell (nod to Bill's code), without adding any custom views.
- (void) layoutSubviews
{
[super layoutSubviews];
// Set top of textLabel to top of cell
CGRect newFrame = self.textLabel.frame;
newFrame.origin.y = CGRectGetMinY (self.contentView.bounds);
[self.textLabel setFrame:newFrame];
// Set top of detailTextLabel to bottom of textLabel
newFrame = self.detailTextLabel.frame;
newFrame.origin.y = CGRectGetMaxY (self.textLabel.frame);
[self.detailTextLabel setFrame:newFrame];
}
Layout of the UITableViewCell textLabel and detailTextLabel are not directly modifiable, except by picking one of the defined styles provided by the API.
typedef enum {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
} UITableViewCellStyle;
If you want to customize UITableViewCell layout, you'll need to subclass it and override the -layoutSubviews method.
- (void) layoutSubviews {
// [super layoutSubViews]; // don't invoke super
... do your own layout logic here
}
Related
I'm in a dilemma which method to use for setting frames of custom UIViews with many subviews in it and still have animations and automatically adjust to rotations. What I usually do when I create a new viewcontroller is alloc my custom view in loadView or viewDidLoad, e.g:
-(void)viewDidLoad
{
[super viewDidLoad];
detailView = [[DetailView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
detailView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth;
self.view = detailView;
}
Normally this width & height is not correct for an iPhone5-screen (the actual view-frame is not set until viewWillAppear) but because of the autoresizingmask it all works out.
Then in the initWithFrame of the custom UIView DetailView, I alloc all subviews with CGRectZero, e.g:
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self)
{
label = [[UILabel alloc] initWithFrame:CGRectZero];
[self addSubview:label];
}
}
Then I override layoutsubviews to set all frames of all subviews. This works perfectly for any screen size and any orientation, e.g:
-(void)layoutSubviews
{
[super layoutSubviews];
label.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
}
However, I just found out that layoutSubviews is not so great when you use animations, because when you use animations in an animationblock, layoutsubviews is called in the middle of the animation and it completely breaks the animation, e.g:
-(void)animateLabel
{
[UIView animateWithDuration:0.4f animations:^
{
label.transform = CGAffineTransformMakeTranslation(100, 100);
}];
}
I believe there are ugly workarounds this by using flags for each animation and in layoutsubviews use those flags to set the correct start or endframe of the animated block but I don't think I should have to create a flag for each animation I want to do.
So my problem is now: how am I supposed to have a custom UIView WITH animations that also automatically adjusts itself to rotations?
The only solution I can come up with right now (that I don't like):
Don't use layoutSubviews but use the setFrame/setBounds method of the custom UIView to set the frames of all subviews. Then check in the viewController every time a rotation occurs and then use the setFrame/setBounds method of the custom UIView to change all frames of all subviews. I don't like this solution because the rotation methods are different in iOS5 and iOS6 and I don't want to have to do this in every UIViewController with it's own custom UIView.
Any suggestions?
I have recently started overriding viewDidLayoutSubviews (many times instead of viewWillAppear) in my UIViewControllers.
Yes viewDidLayoutSubviews is called on rotations. (from comment)
The method fires after all the internal layouts have already been completed so all finalized frames should be setup, but still give you the time you need to make adjustments before the the view is visible and shouldn't have any issues with animations because you are not already inside an animation block.
viewcontroller.m
- (void)viewDidLayoutSubViews {
// At this point I know if an animation is appropriate or not.
if (self.shouldRunAnimation)
[self.fuView runPrettyAnimations];
}
fuView.m
- (void)runPrettyAnimations {
// My animation blocks for whatever layout I'd like.
}
- (void)layoutSubviews {
// My animations are not here, but non animated layout changes are.
// - Here we have no idea if our view is visible to the user or may appear/disappear
// partway through an animation.
// - This also might get called far more than we intend since it gets called on
// any frame updates.
}
On a UITableViewCell with UITableViewCellStyleSubtitle styling, I'm setting the imageView.image, textLabel.text, and detailTextLabel.text. There's white padding all around the cell. How do I get rid of the padding so all my images touch each other like the Youtube app below?
Probably the easiest way to do this would be to subclass UITableViewCell and override the -layoutSubviews method to do something like this:
- (void)layoutSubviews {
//have the cell layout normally
[super layoutSubviews];
//get the bounding rectangle that defines the position and size of the image
CGRect imgFrame = [[self imageView] frame];
//anchor it to the top-left corner
imgFrame.origin = CGPointZero;
//change the height to be the height of the cell
imgFrame.size.height = [self frame].size.height;
//change the width to be the same as the height
imgFrame.size.width = imgFrame.size.height;
//set the imageView's frame (this will move+resize it)
[[self imageView] setFrame:imgFrame];
//reposition the other labels accordingly
}
Just remove table separator:
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
In iOS8 you can set
tableView.layoutMargins = UIEdgeInsets.zero
in code, or from Interface Builder.
Try reducing the UITableView's row height in interface builder or code so that there is no padding.
I had to increase the padding i did so in the interface builder for the tableview.
However Daves answer might give you more control over modifying the cell view.
I have made a custom UIView which is shown when the user hits a button in the navigationbar. I make my view's in code. In my loadview I set the autoresizing masks and the view loads correct on screen. However the UIView which is shown when the user taps the button does not resize even when I have set the autoresizing masks.
UIView *blackView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 416.0)];
blackView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
Do I need to use self.view.frame.size.width and self.view.frame.size.height instead? And if I do why? Does not resizing masks work outside of loadView?
Thank you for your time:)
the autoresizingMask affects how a view will behave when its superviews frame changes. if all you are doing is showing theblackViewwhen you tap a button, thenblackView` will have whatever frame you initially set for it.
If this isn't enough info, please post some more code around how you are configuring and displaying blackView and it's superview and explain more about what situations you are expecting blackView to resize in. Rotation is one of them, if that's what you're concerned with.
First things first, I hope you've done this:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return YES;
}
Let's say the view that needs resizing is: view2
The view that has view2 as a subview is: view1
While creating view1 you would declare it as:
view1 = [[UIView alloc] init];
[view1 setNeedsLayout];
Now in view1's .m file you need to overload the layoutSubviews method as shown:
- (void)layoutSubviews
{
CGRect frame = view2.frame;
// apply changes to frame
view2.frame = frame;
}
In case view1 is a view controller's view, you need to do that same thing as above in the willRotate method as shown
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
CGRect frame = view2.frame;
// apply changes to frame
view2.frame = frame;
}
This is a tried and tested method that I use to handle orientation changes.
Setup: I have a UITextView inside a UITableViewCell's contentView. I want it to take up the full size of the cell. I create the text view like so:
UITextView *textView = [[[UITextView alloc] initWithFrame:CGRectMake(0,0,268,43)] autorelease];
textView.backgroundColor = [UIColor redColor];
textView.layer.cornerRadius = 10;
textView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
And I override heightForRowAtIndexPath to return 200 for that row.
The background color is just so I can tell where it is. The cell seems to be automatically sizing it correctly upon first view. However, I need it to continue to resize it correctly while autorotating the interface, which only seems to work sometimes, and only when I'm not editing the textView. Other times, it resizes the view to have a very small height (looks like -1), or makes it too wide, or just doesn't even resize it at all.
I've tried overriding layoutSubviews in the cell and just do nothing, but even that doesn't stop the view from resizing all over the place.
I've been hacking away at this for awhile now, but still have found no solution.
A UITableViewCell has a fixed height, the height provided by the UITableView's delegate. When you rotate your device, the height of the row will never change, unless you call -reloadData on your tableView. I'd get rid of the autoresizing and manage it yourself.
When you init your textField, you can easily set the frame to CGRectZero. Then implement -layoutSubviews (and call super in that method, before setting the frames of your subviews) and set the frame of the UITextField according to the contentRect property of the cell.
- (id)initWithFrame:(CGRect)frame reuseIdentifier:(NSString *)reuseIdentifier {
if(self = [super ...]){ // or -initWithStyle:reuseIdentifier: whatever you want
_textView = [[UITextView alloc] initWithFrame:CGRectZero]; // Instance variable
// Probably not needed to set autoresizing mask
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
_textView.frame = CGRectMake(0.0f, 0.0f, self.contentRect.size.width, self.contentRect.size.height); // Adjust as needed
}
If you have a plain (not grouped) UITableView with a single row, the rest of the screen is filled with blank or empty cells. How do you change the appearance of these blank cells? Af first, I thought they would have the appearance of the cell used in the table view but it seems they don't.
The people from Cultured Code (Things) have done a nice job modifying these cells but I can't immediately think of a way to change their appearance.
Any tips?
Although it's not quite what you're looking for, you can also change it to just display a blank region (instead of blank cells) by setting a footer view on the table. A UIView of height 0 will do the trick.
Based on samvermette's answer, but modified to use a background image directly rather than compositing an image from a UITableViewCell subclass. Sam's answer is good, but it will be less performant than just creating a background image directly.
Create a UIView subclass -- in this example I called it CustomTiledView -- and add the following code to the class:
- (void)drawRect:(CGRect)rect {
UIImage *image = [UIImage imageNamed:#"tableview_empty_cell_image"];
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM (context, 1, -1);
CGContextDrawTiledImage(context,
CGRectMake(0, 0, rect.size.width, image.size.height),
[image CGImage]);
}
Add the following code to your tableviewcontroller:
- (CGFloat)tableView:(UITableView *)tableView
heightForFooterInSection:(NSInteger)section {
// this can be any size, but it should be 1) multiple of the
// background image height so the last empty cell drawn
// is not cut off, 2) tall enough that footer cells
// cover the entire tableview height when the tableview
// is empty, 3) tall enough that pulling up on an empty
// tableview does not reveal the background.
return BACKGROUND_IMAGE_HEIGHT * 9; // create 9 empty cells
}
- (UIView *)tableView:(UITableView *)tableView
viewForFooterInSection:(NSInteger)section {
CustomTiledView *footerView = [[CustomTiledView alloc] init];
return [footerView autorelease];
}
Finally, you'll need to set the bottom content inset of your tableview to the negative of the value returned from -tableView:heightForFooterInsection: In this example it would be -1*BACKGROUND_IMAGE_HEIGHT*9. You can set the bottom content inset either from the Size Inspector of Interface Builder or by setting the self.tableView.contentInset property from the tableviewcontroller.
Cheers!
I set a ~300px-high UIView subclass to my tableView header and footer views, adjusting the tableView insets so they compensate for these views (set the top and bottom insets to -300px).
My UIView subclass implements the drawRect method, in which I use CGContextDrawTiledImage() to draw an empty UITableViewCell repetitively:
- (void)drawRect:(CGRect)rect {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(300, 46),NO,0.0);
emptyCell = [[SWTableViewCell alloc] initWithFrame:CGRectZero];
[emptyCell drawRect:CGRectMake(0, 0, 300, 46)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
[emptyCell release];
UIGraphicsEndImageContext();
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextScaleCTM (context, 1, -1); // this prevents the image from getting drawn upside-down
CGContextDrawTiledImage(context, CGRectMake(0, 0, 300, 46), [newImage CGImage]);
}
In my case my tableviewcells are 46px high, so if I want make UIView subclass to contain 8 of these empty cells, I need to make it 368px high.
From http://jomnius.blogspot.com/2011/03/hide-uitableview-empty-cell-separator.html
Easy way is to define that table has no cell separator lines:
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
Hard way, if you need separator lines, is to define that table has no separator lines and create cell separator lines as part of custom UITableViewCell. Bottom part of cell, obviously, and most likely using really thin graphics image. Remember to define image autosizing and mode properly.
Another way is to define an empty UITableView's tableFooterView:
UIView *footer =
[[UIView alloc] initWithFrame:CGRectZero];
self.myTable.tableFooterView = footer;
[footer release];
If you simply want to change the height of the "empty cells" when there are no cells at all in your TableView: you need to set the rowHeight property of your UITableView (the implementation of tableView:heightForRowAtIndexPath: has no effect on empty TableViews). If there are cells in your TableView and you have implemented tableView:heightForRowAtIndexPath: then the last row height will be used for empty cells.
You will probably have to make a custom subclass of UITableView that fills the background with the appearance of blank cells.
- (void)drawRect:(CGRect)rect {
[super drawRect: rect];
// draw your background here if the rect intersects with the background area
}
I believe the approach of using the UITableView tableView:viewForFooterInSection: method will not work for the case where the table is empty, ie., with no cells. In that case, you can use the technique Answerbot proposed can be repurposed to stuff the created UIView into the table's footer view explicitly, like the following:
CGRect cellRect = CGRectMake(0, 0, table.bounds.size.width, table.bounds.size.height);
CustomTiledView *footerView = [[[CustomTiledView alloc] initWithFrame:cellRect] autorelease];
table.tableFooterView = footerView;
For swift 4.0 :
self.tableView.tableFooterView = UIView(frame: CGRect.zero)