iOS Centring UITableViewCell label with disclosure indicator - ios

I am looking for a way to enter a label within a table cell that also has a disclosure indicator. The problem i'm having at the moment is that it seems like the disclosure indicator is being ignored when calculating the label's positions
Heres a picture:
So as you can see the label is centred in the area between the left side of the cell and the left side of the indicator, if it was centred in the cell it would sit below the nav bar heading.
Any help is appreciated thankyou
From within the storyboard

Okay, first an explanation for your issue. It has to do with the anatomy of a UITableViewCell. With anatomy, I mean the fact that the UITableViewCell for you is just a container for another container, which is the contentView (you can also see this one in your storyboard).
When you are operating in Storyboards, you are solely operating on the contentView, not on the actual UITableViewCell. So, when you setup your UILabel to be centered on the X-axis with AutoLayout, AutoLayout will always try to center it within the contentView, not in the outer container (i.e. the UITableViewCell). Then, when you add a disclosure indicator to the UITableViewCell, the contentView automatically gets shrinked in its width because the cell makes space for the disclosure indicator and wants to prevent you from adding UI elements in the right area that is reserved for the disclosure indicator.
Now, you have a few options around this:
you can edit the constraint directly and add a constant to it (which has to be the same value that the label gets shifted when you'd remove the indicator)
don't use the default disclosure indicator (i.e. don't tick the checkbox in Storyboards) and just add a UIImageView with an image that looks identical.

To not be bound to any constants you can calculate the difference in widths of frame and contentView.frame. So first create an outlet collection like so:
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *centerConstraintsToOffset;
Then add the center constraints that you want to be centered horizontally in cell to that outlet collection:
And finally add this code to your cell:
override func layoutSubviews() {
super.layoutSubviews()
for constraint in centerConstraintsToOffset {
constraint.constant = (frame.size.width - contentView.frame.size.width) / 2.0
}
}
This also gives you flexibility of adding or removing cell accessories on the go, and your views will always be perfectly center aligned. Even if you remove the accessory at all.

Pavel's answer fixed the issue for me. After creating the IBOutlet collection as his answer demonstrates, here is his code example edited for Swift 3:
override func layoutSubviews() {
super.layoutSubviews()
for constraint: NSLayoutConstraint in self.centerConstraintsToOffset {
constraint.constant = (frame.size.width - contentView.frame.size.width) / 2.0
}
}

As you've already noticed, adding a 'UITableViewCellAccessoryDisclosureIndicator' will shrink the space allotted for your cell's contentView. Another solution that doesn't require a custom indicator or guessing at an offset would be to programmatically add a UILabel to the root view of the cell, not the contentView. For example:
#property UILabel *label;
// ...
- (void)layoutSubviews
{
[super layoutSubviews];
[self.label removeFromSuperview];
self.label = [[UILabel alloc] init];
self.label.text = #"Motorsport";
[self.label sizeToFit];
label.center = CGPointMake(self.center.x, self.size.height/2);
[self addSubview:self.label];
}

Related

How to make a UIView expand to contain a UIImageView or a UITextView with auto layout and IB?

I have a UIView that contains two subviews - a UIImageView or a UITextView. The UIImageView has a fixed height and width.
The UITextView has variable size.
Only one of the UIImageView and UITextView would be displayed at a time. I plan to accomplish this programmatically by setting hidden = YES.
I would like the UIView to hug whichever child view is not hidden with no margin.
I would like to accomplish this with IB and autolayout if possible because the rest of the view is built this way.
So far I have created constraints that link the 4 edges of both of the subviews to the parent view and constraints for the height and width of the UIImageView. Naturally this creates a content priority ambiguity.
I would appreciate any advice.
Thank you!
So the contain view has to have trailing constraint and constraint with bottom which should have IBOutlet(s) references in viewcontroller and then when the text changes :
- (void)textViewDidChange:(UITextView *)textView {
[self.textView sizeToFit];
self.containerViewTrailingConstraint.constant = self.textView.contentSize.width;
self.containerViewConstraintWithBottom.constant = self.textView.contentSize.height;
[self.view layoutIfNeeded];
}
And when the textView is hidden then you have to set the self.containerViewTrailingConstraint.constant, self.containerViewConstraintWithBottom.constant in relation with the imageView

Updating width of uilabel when subview not

I have UILabel in a UITableViewCell whose neighboring subview is UIControlSwitch. When the UIControlSwitch is hidden, the UILabel's width is expected to grow. Below is my custom class implementation:
- (void)setBounds:(CGRect)bounds
{
[super setBounds:bounds];
self.contentView.frame = self.bounds;
}
- (void)layoutSubviews
{
[super layoutSubviews];
[self.contentView updateConstraintsIfNeeded];
[self.contentView layoutIfNeeded];
self.numberLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.numberLabel.frame);
self.quoteLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.quoteLabel.frame);
}
#end
I have uploaded a sample code to explain the constraints on the storyboard.
The UILabel's width is not changing. If I set a trailing space between the UILabel and the UIControlSwitch, the control is partially hidden beyond the screen bounds. Hence the constraint for the UILabel has trailing space to the superview instead of the UIControlSwitch.
When you hide a view, it still takes part in the layout process. So simply hiding your UISwitch will not cause anything to change.
So you either have to add/remove constraints, remove the UISwitch from its containing view or change how you are doing the constraints.
Option 1: Instead of hide/show do remove/add.
If you put the UISwitch in its own view, you can pin the UILabel to the switches containing view. Instead of hiding the UISwitch, remove it from its containing view. The result will be that the containing view collapses and your UILabel will stretch as long as the label is pinned to the switches view and the switches view to the edge.
Instead of show, add the UILabel back into the container view. The view will stretch and your UILabel will shrink.
Option 2: Use simple trailing constraint for UILabel.
Make your UILabel have a trailing constraint to the superview and CTRL drag that constraint into your code so you can set its value. Or you could add it manually if that is what you are doing.
Do not pin the UILabel to the UISwitch at all. Pin the UILabel to the right h§and side so it has the required position.
So your UILabel is now pinned to the right edge with an offset of your choosing. If you set this small, the UILabel will be over/under the switch. If set to the width of the switch plus left and right margin it will appear to stretch up to the switch.
In your code, you can find the width of the UISwitch by looking at its frame.
1) When the UISwitch is to be visible, set the trailing pin constraint of the UILabel to be the margin you want + width of UISwitch + margin to left of UISwitch you want.
2) When the UISwitch is to be hidden, set the trailing pin constraint of the UILabel to be the margin you want to the superview.
This should then cause the UILabel to extend to the switch when not hidden, or to the edge when the switch is hidden.

Create tableHeaderView that always fills the frame of the visible table?

I need to implement the ability to display a label perfectly centered on screen with nothing else visible, but this needs to be done in a UITableView. The setup is a UISpiltViewController that has a UITableViewController for the detail view controller, and when no item is selected on the left I want to display a message stating that on the right, and when the user selects an item that label should instantly disappear and reveal the table. (Just like the Mail app.)
I already have this set up and it's working ok, but for some reason it's not always staying centered on screen, and it isn't a very good solution - there are some minor oddities for example you can partially see the top of the table while the rotation is occurring. I am just creating a UILabel, setting its frame to fill the visible area, then setting the table's tableHeaderView to that label, and finally disabling the ability to scroll the table. And upon rotation, the frame has to be updated to fill the visible area again. That's where the oddities occur, because it's not updating until after the rotation completes.
My question is, what is a better approach to implement this behavior? Is there some way I can prevent having to update the frame after rotation, would it be possible to use Auto Layout for the tableHeaderView?
//Setting the tableHeaderView
self.tableView.tableHeaderView = self.label;
//Creating the UILabel
- (UILabel *)label {
_label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, self.tableView.bounds.size.width,
self.tableView.bounds.size.height
- self.navigationController.navigationBar.frame.size.height
- self.tabBarController.tabBar.frame.size.height
- MIN([UIApplication sharedApplication].statusBarFrame.size.height,
[UIApplication sharedApplication].statusBarFrame.size.width))];
_label.text = #"Nothing Selected";
_label.textAlignment = NSTextAlignmentCenter;
_label.backgroundColor = self.tableView.backgroundColor;
return _label;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
if (_label) {
_label.frame = CGRectMake(0, 0, self.tableView.bounds.size.width,
self.tableView.bounds.size.height
- self.navigationController.navigationBar.frame.size.height
- self.tabBarController.tabBar.frame.size.height
- MIN([UIApplication sharedApplication].statusBarFrame.size.height,
[UIApplication sharedApplication].statusBarFrame.size.width));
self.tableView.tableHeaderView = nil;
self.tableView.tableHeaderView = _label;
}
}
Is your detail controller a UITableViewController? If so, that makes things harder since any subviews you add (your label) become part of the table. It would be easier if you use a UIViewController, and alternately hide the label or table view when you need to. The label can be any size, and use centerX and centerY constraints to keep it centered. If you do it that way, you won't have to do anything on rotation.
You don't have to set the label as the headerView of your UITableView.
You can simply add the label to self.view. Even if you are running inside a UITableViewController, each UIViewController always has a view property.
Add the label to self.view and toggle the visibility of the label and the tableView as needed.This is much easier, than trying to fiddle the label in the tableview - hierarchy ;)
EDIT
As pointed out by #rdelmar, the UITableViewController indeed does not have a separate view which contains the tableview, but rather uses the tableview as it's view directly. -.-
Sorry. I did assume the two views were separated.
All I can say is follow #rdelmar s advice, and stop using UITableViewController. It forces you to do things the default way, and if you want to customized it you WILL have a bad time :(
EDIT 2
Ok so you have 2 options:
1) Do the following:
[_label setAutoResizingMask:(UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleHeight)];
in your label getter. That way, you don't have to care about screen rotation anymore. The label will always fit its parents bounds.
2) Use UIViewController and treat the label as a sibling of the tableView. <- Better approach but requires more refactoring from your current state.
As pointed out by Cabus, the solution is to set the autoresizingMask of the label to UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleHeight. That way, the label will always fit its parent. This can be done while using the label for the tableHeaderView, and there's no longer a need for detecting orientation changes.

Scrolling ViewController Content

What I have in the view controller view are :
An image of fixed height
Few labels
Table view with n rows.
Once rendered I want everything here to be inside the scroll the view so the user can scroll the entire screen as needed. Note that the scrollView needs to expand to the entire size of the tableView to show its full contents. I have tried different ways of doing this but unable to do it. I would appreciate any pointers or code segment to get this done.
There are essentially two ways to do so.
tableHeaderView
The first way involves the tableHeaderView property of the UITableView instance you have. You can simply add the UITableView with the constraints/frame/autoresizingMask that allows you to put it full-screen. Done that, you simply do (i.e. in your viewDidLoad):
UIView *headerView = [UIView new];
// Here I am supposing that you have a 200pt high view and a `self.tableView` UITableView
headerView.frame = CGRectMake(0, 0, self.tableView.frame.size.width, 200.0);
UIImageView *fixedImageView = [UIImageView new];
// configure your imageView..
[headerView addSubview:fixedImageView];
// configure labels as you want and add them to headerView as subviews
// Now set `UITableView` headerView
self.tableView.tableHeaderView = headerView;
If you want to use AutoLayout for your tableHeaderView, I suggest you to take a look at this question
Dynamic scrollView
Another way to do this is to to create an UIScrollView, put everything inside, and let it scroll. The downside of this method is that if you are using floating section headers for your UITableView, they will not float due to the fact that the tableView is going to stay fixed, while the parent scrollView is going to scroll.
On the other side, this approach is more AutoLayout friendly due to the fact you can use constraints easily.
To do so, you start adding an UIScrollView to your view, and placing all your other views inside it.
Be sure to add a Vertical Spacing constraint between the first view inside your scrollView (I suppose the UIImageView) and the scrollView top, and between the last view (I suppose the UITableView) and the scrollView bottom, to avoid an ambiguous content size.
You should have something like that (I omitted the labels for the sake of brevity):
Note that every view is inside a parent UIScrollView
After that, add an Height constraint to the tableView, and add an IBOutlet to your view controller subclass, i.e. like this:
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *tableViewHeightConstraint;
Now you only need to configure this constraint to reflect the tableView natural height, given by its rows, etc. To do so, you simply calculate the height in this way:
// Resize TableView
CGFloat height = self.tableView.contentSize.height;
self.tableViewHeightConstraint.constant = height;
Now the tableView will resize, and due to its constraints it will adapt the parent scrollView contentSize.
Just be sure to refresh this height constraint anytime you reload the UITableView dataSource.

IOS: Adjust UIButton height depend on title text using Autolayout?

I have a UIButton and it can change the title at the runtime. Therefore, I want to increase the UIButton height depend on the title text for display full text by using AutoLayout.
I can increase the UILabel height by set the height constraint to "Greater than or Equal" but it not work with UIButton.
I have used [myButton sizeToFit] but it only increase the UIButon width (not increase height).
My current UIButton properties now is
- constraint height: 30
- leading : 15
- trailing: 15
- top: 5
- fontsize: 12
UPDATE
I created an IBOutlet for constraint height of UIButton for changing the height as #NSNood said.
Then I need to use \n in title text to split line.
But I don't know where should I put the \n?
Here is the Button that I want in portrait
Here is the Button that I want in landscape
How can I determine the place to put \n?
Please guide me how to achieve it with AutoLayout. Any help would be appreciated.
Sorry that I didn't follow the post, lately and thus am coming up with a real late solution. Still I'm writing the answer as a reference, if someone might find it useful in future.
First of all let's show the storyboard configuration for the button. Those are depicted in the following pictures:
The picture shows that I have added only left, top and right constraints for the button and nothing else. This allows the button to have some intrinsicContentSize for it's height but it's width is still determined by it's left and right constraints.
The next phase is to write some ViewController class that shall contain the button. In my VC, I have created an outlet for the button by name button:
#property(nonatomic,weak) IBOutlet UIButton* button;
and has attached it to the storyboard button. Now I have overridden two methods, namely, viewDidLoad and viewWillLayoutSubviews like below:
-(void)viewDidLoad {
[super viewDidLoad];
self.button.titleLabel.numberOfLines = 0;
self.button.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
}
-(void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self.button setTitle:#"Chapter One\n "
"A Stop on the Salt Route\n "
"1000 B.C.\n "
"As they rounded a bend in the path that ran beside the river, Lara recognized the silhouette of a fig tree atop a nearby hill. The weather was hot and the days were long. The fig tree was in full leaf, but not yet bearing fruit." forState:UIControlStateNormal];
}
The viewDidLoad method ensures the titleLabel (the label that
holds button text) is multiline and if some large text comes to it,
it wraps the text by wrapping words.
The viewWillLayoutSubviews method ensures button layouting process
occurs whenever bounds of the main view change, e.g. due to the
change of interface orientation.
The final and the most effective part is to manually handle the layout process for the button. For this purpose, we need to subclass UIButton. I have written a subclass named MyButton that inherits from UIButton and you might use whatever name you like. Set this as the custom class for the button in Identity Inspector.
The subclass overrides two methods, namely, intrinsicContentSize and layoutSubviews. The class body looks something like the following:
#import "MyButton.h"
#implementation MyButton
-(CGSize)intrinsicContentSize {
return [self.titleLabel sizeThatFits:CGSizeMake(self.titleLabel.preferredMaxLayoutWidth, CGFLOAT_MAX)];;
}
-(void)layoutSubviews {
self.titleLabel.preferredMaxLayoutWidth = self.frame.size.width;
[super layoutSubviews];
}
#end
The UIButon subclass takes the ownership of the layout process by overriding layoutSubviews method. The basic idea here is to determine the button width, once it has been layout. Then setting the width as preferredMaxLayoutWidth (the maximum width for layouting engine, that a multiline label should occupy) of it's child titleLabel (the label that holds button text). Finally, returning an intrinsicContentSize for the button based on it's titleLabel's size, so that the button fully wraps it's titleLabel.
The overridden layoutSubviews is called when the button is already
layed out and it's frame size is determined. At it's first step,
button's rendered width is set as preferredMaxLayoutWidth of the
button's titleLabel.
The second step re-invokes the layouting engine by calling [super
layoutSubviews], so that the buttons intrinsicContentSize is
re-determined based on it's titleLabel's
preferredMaxLayoutWidth, which is set to buttons rendered width,
by now.
In the overridden intrinsicContentSize method we return the
minimum fitting size for the button that fully wraps it's
titleLabel with preferredMaxLayoutWidth set. We use
sizeThatFits fits method on the button's titleLabel and that
simply works as titleLabel doesn't follow any constraint based
layout.
The outcome should be something similar to that you might have required.
Feel free to let me know about any other clarification/concern.
Thanks.
Ayan Sengupta solution in Swift, with support for contentEdgeInsets (thanks Claus Jørgensen):
(You may also further customize the code to take titleEdgeInsets into account if needed)
Subclass your UIButton to take the ownership of the layout process:
/// https://stackoverflow.com/a/50575588/1033581
class AutoLayoutButton: UIButton {
override var intrinsicContentSize: CGSize {
var size = titleLabel!.sizeThatFits(CGSize(width: titleLabel!.preferredMaxLayoutWidth - contentEdgeInsets.left - contentEdgeInsets.right, height: .greatestFiniteMagnitude))
size.height += contentEdgeInsets.left + contentEdgeInsets.right
return size
}
override func layoutSubviews() {
titleLabel?.preferredMaxLayoutWidth = frame.size.width
super.layoutSubviews()
}
}
Use this class in your storyboard, and set constraints for Leading, Trailing, Top, Bottom. But don't set any Height constraint.
An alternative without subclassing is to add a wrapper view as suggested by Bartłomiej Semańczyk answer and Timur Bernikowich comment.
The point is that if you set sizeToFit property, then the text will always be in one line and the width of the button will increase unless you put a next-line sign \n to explicitly say that you want it to be several lines.
You put '\n' in the end of the first line like "line \n line" which represents
line
line
If you want to have two different string values (with \n positioned differently) for Portrait and Landscape you can check the orientation condition using UIDeviceOrientation (UIDevice.currentDevice.orientation) described here and set a string value depending on the orientation of the device
There is a way I always used:
Add another reference UILabel which lineNumber=0 and the same width with the target button.
Do not set height constraint for the ref-UILable, and should set a height constraint for the button to adjust its height
Set the same text to the ref UILabel with the button.titleLable, sizeTofit it and get its frame.size.height
Use the height value to the height constraint of the target button. (Of course, the button.titleLabel linenumber should be set to 0 or more lines)
Done. :)
PS1. This way can be used for the button and ref-label in a scrollview.
PS2. In some case, we can not get the correct height of the ref-label because it cannot gain a correct frame.width in scrollview, especially when we use the trailling constraint. We could consider to define a fixed width to the ref-label before sizeTofit and obtain the correct height for target button use.

Resources