UITableViewCell with UITextView height in iOS 7? - cocoa-touch

How can I calculate the height of an UITableViewCell with an UITextView in it in iOS 7?
I found a lot of answers on similar questions, but sizeWithFont: takes part in every solution and this method is deprecated!
I know I have to use - (CGFloat)tableView:heightForRowAtIndexPath: but how do I calculate the height my TextView needs to display the whole text?

First of all, it is very important to note, that there is a big difference between UITextView and UILabel when it comes to how text is rendered. Not only does UITextView have insets on all borders, but also the text layout inside it is slightly different.
Therefore, sizeWithFont: is a bad way to go for UITextViews.
Instead UITextView itself has a function called sizeThatFits: which will return the smallest size needed to display all contents of the UITextView inside a bounding box, that you can specify.
The following will work equally for both iOS 7 and older versions and as of right now does not include any methods, that are deprecated.
Simple Solution
- (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;
}
This function will take a NSAttributedString and the desired width as a CGFloat and return the height needed
Detailed Solution
Since I have recently done something similar, I thought I would also share some solutions to the connected Issues I encountered. I hope it will help somebody.
This is far more in depth and will cover the following:
Of course: setting the height of a UITableViewCell based on the size needed to display the full contents of a contained UITextView
Respond to text changes (and animate the height changes of the row)
Keeping the cursor inside the visible area and keeping first responder on the UITextView when resizing the UITableViewCell while editing
If you are working with a static table view or you only have a known number of UITextViews, you can potentially make step 2 much simpler.
1. First, overwrite the heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// check here, if it is one of the cells, that needs to be resized
// to the size of the contained UITextView
if ( )
return [self textViewHeightForRowAtIndexPath:indexPath];
else
// return your normal height here:
return 100.0;
}
2. Define the function that calculated the needed height:
Add an NSMutableDictionary (in this example called textViews) as an instance variable to your UITableViewController subclass.
Use this dictionary to store references to the individual UITextViews like so:
(and yes, indexPaths are valid keys for dictionaries)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Do you cell configuring ...
[textViews setObject:cell.textView forKey:indexPath];
[cell.textView setDelegate: self]; // Needed for step 3
return cell;
}
This function will now calculate the actual height:
- (CGFloat)textViewHeightForRowAtIndexPath: (NSIndexPath*)indexPath {
UITextView *calculationView = [textViews objectForKey: indexPath];
CGFloat textViewWidth = calculationView.frame.size.width;
if (!calculationView.attributedText) {
// This will be needed on load, when the text view is not inited yet
calculationView = [[UITextView alloc] init];
calculationView.attributedText = // get the text from your datasource add attributes and insert here
textViewWidth = 290.0; // Insert the width of your UITextViews or include calculations to set it accordingly
}
CGSize size = [calculationView sizeThatFits:CGSizeMake(textViewWidth, FLT_MAX)];
return size.height;
}
3. Enable Resizing while Editing
For the next two functions, it is important, that the delegate of the UITextViews is set to your UITableViewController. If you need something else as the delegate, you can work around it by making the relevant calls from there or using the appropriate NSNotificationCenter hooks.
- (void)textViewDidChange:(UITextView *)textView {
[self.tableView beginUpdates]; // This will cause an animated update of
[self.tableView endUpdates]; // the height of your UITableViewCell
// If the UITextView is not automatically resized (e.g. through autolayout
// constraints), resize it here
[self scrollToCursorForTextView:textView]; // OPTIONAL: Follow cursor
}
4. Follow cursor while Editing
- (void)textViewDidBeginEditing:(UITextView *)textView {
[self scrollToCursorForTextView:textView];
}
This will make the UITableView scroll to the position of the cursor, if it is not inside the visible Rect of the UITableView:
- (void)scrollToCursorForTextView: (UITextView*)textView {
CGRect cursorRect = [textView caretRectForPosition:textView.selectedTextRange.start];
cursorRect = [self.tableView convertRect:cursorRect fromView:textView];
if (![self rectVisible:cursorRect]) {
cursorRect.size.height += 8; // To add some space underneath the cursor
[self.tableView scrollRectToVisible:cursorRect animated:YES];
}
}
5. Adjust visible rect, by setting insets
While editing, parts of your UITableView may be covered by the Keyboard. If the tableviews insets are not adjusted, scrollToCursorForTextView: will not be able to scroll to your cursor, if it is at the bottom of the tableview.
- (void)keyboardWillShow:(NSNotification*)aNotification {
NSDictionary* info = [aNotification userInfo];
CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0, kbSize.height, 0.0);
self.tableView.contentInset = contentInsets;
self.tableView.scrollIndicatorInsets = contentInsets;
}
- (void)keyboardWillHide:(NSNotification*)aNotification {
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.35];
UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0, 0.0, 0.0);
self.tableView.contentInset = contentInsets;
self.tableView.scrollIndicatorInsets = contentInsets;
[UIView commitAnimations];
}
And last part:
Inside your view did load, sign up for the Notifications for Keyboard changes through NSNotificationCenter:
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
}
Please don't get mad at me, for making this answer so long. While not all of it is needed to answer the question, I believe that there are other people who these directly related issues will be helpful to.
UPDATE:
As Dave Haupert pointed out, I forgot to include the rectVisible function:
- (BOOL)rectVisible: (CGRect)rect {
CGRect visibleRect;
visibleRect.origin = self.tableView.contentOffset;
visibleRect.origin.y += self.tableView.contentInset.top;
visibleRect.size = self.tableView.bounds.size;
visibleRect.size.height -= self.tableView.contentInset.top + self.tableView.contentInset.bottom;
return CGRectContainsRect(visibleRect, rect);
}
Also I noticed, that scrollToCursorForTextView: still included a direct reference to one of the TextFields in my project. If you have a problem with bodyTextView not being found, check the updated version of the function.

There is a new function to replace sizeWithFont, which is boundingRectWithSize.
I added the following function to my project, which makes use of the new function on iOS7 and the old one on iOS lower than 7. It has basically the same syntax as sizeWithFont:
-(CGSize)text:(NSString*)text sizeWithFont:(UIFont*)font constrainedToSize:(CGSize)size{
if(IOS_NEWER_OR_EQUAL_TO_7){
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
font, NSFontAttributeName,
nil];
CGRect frame = [text boundingRectWithSize:size
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:attributesDictionary
context:nil];
return frame.size;
}else{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
return [text sizeWithFont:font constrainedToSize:size];
#pragma clang diagnostic pop
}
}
You can add that IOS_NEWER_OR_EQUAL_TO_7 on your prefix.pch file in your project as:
#define IOS_NEWER_OR_EQUAL_TO_7 ( [ [ [ UIDevice currentDevice ] systemVersion ] floatValue ] >= 7.0 )

If you're using UITableViewAutomaticDimension I have a really simple (iOS 8 only) solution. In my case it's a static table view, but i guess you could adapt this for dynamic prototypes...
I have a constraint outlet for the text-view's height and I have implemented the following methods like this:
// Outlets
#property (weak, nonatomic) IBOutlet UITextView *textView;
#property (weak, nonatomic) IBOutlet NSLayoutConstraint *textViewHeight;
// Implementation
#pragma mark - Private Methods
- (void)updateTextViewHeight {
self.textViewHeight.constant = self.textView.contentSize.height + self.textView.contentInset.top + self.textView.contentInset.bottom;
}
#pragma mark - View Controller Overrides
- (void)viewDidLoad {
[super viewDidLoad];
[self updateTextViewHeight];
}
#pragma mark - TableView Delegate & Datasource
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 80;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
#pragma mark - TextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
[self.tableView beginUpdates];
[self updateTextViewHeight];
[self.tableView endUpdates];
}
But remember: the text view must be scrollable, and you must setup your constraints such that they work for automatic dimension:
setup all the view in the cell in relation to each other, with fixed heights (including the text view height, which you will change programatically)
the top most view has the top spacing and the bottom most view has the bottom spacing to the super view;
The most basic cell example is:
no other views in the cell except the textview
0 margins around all sides of the text view and a predefined height constraint for the text view.

Tim Bodeit's answer is great. I used the code of Simple Solution to correctly get the height of the text view, and use that height in heightForRowAtIndexPath. But I don't use the rest of the answer to resize the text view. Instead, I write code to change the frame of text view in cellForRowAtIndexPath.
Everything is working in iOS 6 and below, but in iOS 7 the text in text view cannot be fully shown even though the frame of text view is indeed resized. (I'm not using Auto Layout). It should be the reason that in iOS 7 there's TextKit and the position of the text is controlled by NSTextContainer in UITextView. So in my case I need to add a line to set the someTextView in order to make it work correctly in iOS 7.
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"7.0")) {
someTextView.textContainer.heightTracksTextView = YES;
}
As the documentation said, what that property does is:
Controls whether the receiver adjusts the height of its bounding
rectangle when its text view is resized. Default value: NO.
If leave it with the default value, after resize the frame of someTextView, the size of the textContainer is not changed, leading to the result that the text can only be displayed in the area before resizing.
And maybe it is needed to set the scrollEnabled = NO in case there's more than one textContainer, so that the text will reflow from one textContainer to the another.

Here is one more solution that aims at simplicity and quick prototyping:
Setup:
Table with prototype cells.
Each cell contains dynamic sized UITextView w/ other contents.
Prototype cells are associated with TableCell.h.
UITableView is associated with TableViewController.h.
Solution:
(1) Add to TableViewController.m:
// This is the method that determines the height of each cell.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// I am using a helper method here to get the text at a given cell.
NSString *text = [self getTextAtIndex:indexPath];
// Getting the height needed by the dynamic text view.
CGSize size = [self frameForText:text sizeWithFont:nil constrainedToSize:CGSizeMake(300.f, CGFLOAT_MAX)];
// Return the size of the current row.
// 80 is the minimum height! Update accordingly - or else, cells are going to be too thin.
return size.height + 80;
}
// Think of this as some utility function that given text, calculates how much
// space would be needed to fit that text.
- (CGSize)frameForText:(NSString *)text sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size
{
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
font, NSFontAttributeName,
nil];
CGRect frame = [text boundingRectWithSize:size
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
attributes:attributesDictionary
context:nil];
// This contains both height and width, but we really care about height.
return frame.size;
}
// Think of this as a source for the text to be rendered in the text view.
// I used a dictionary to map indexPath to some dynamically fetched text.
- (NSString *) getTextAtIndex: (NSIndexPath *) indexPath
{
return #"This is stubbed text - update it to return the text of the text view.";
}
(2) Add to TableCell.m:
// This method will be called when the cell is initialized from the storyboard
// prototype.
- (void)awakeFromNib
{
// Assuming TextView here is the text view in the cell.
TextView.scrollEnabled = YES;
}
Explanation:
So what's happening here is this: each text view is bound to the height of the table cells by vertical and horizontal constraints - that means when the table cell height increases, the text view increases its size as well. I used a modified version of #manecosta's code to calculate the required height of a text view to fit the given text in a cell. So that means given a text with X number of characters, frameForText: will return a size which will have a property size.height that matches the text view's required height.
Now, all that remains is the update the cell's height to match the required text view's height. And this is achieved at heightForRowAtIndexPath:. As noted in the comments, since size.height is only the height for the text view and not the entire cell, there should be some offset added to it. In the case of the example, this value was 80.

One approach if you're using autolayout is to let the autolayout engine calculate the size for you. This isn't the most efficient approach but it is pretty convenient (and arguably the most accurate). It becomes more convenient as the complexity of the cell layout grows - e.g. suddenly you have two or more textviews/fields in the cell.
I answered a similar question with a complete sample for sizing tableview cells using auto layout, here:
How to resize superview to fit all subviews with autolayout?

The complete smooth solution is as follows.
First, we need the cell class with a textView
#protocol TextInputTableViewCellDelegate <NSObject>
#optional
- (void)textInputTableViewCellTextWillChange:(TextInputTableViewCell *)cell;
- (void)textInputTableViewCellTextDidChange:(TextInputTableViewCell *)cell;
#end
#interface TextInputTableViewCell : UITableViewCell
#property (nonatomic, weak) id<TextInputTableViewCellDelegate> delegate;
#property (nonatomic, readonly) UITextView *textView;
#property (nonatomic) NSInteger minLines;
#property (nonatomic) CGFloat lastRelativeFrameOriginY;
#end
#import "TextInputTableViewCell.h"
#interface TextInputTableViewCell () <UITextViewDelegate> {
NSLayoutConstraint *_heightConstraint;
}
#property (nonatomic) UITextView *textView;
#end
#implementation TextInputTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
_textView = [UITextView new];
_textView.translatesAutoresizingMaskIntoConstraints = NO;
_textView.delegate = self;
_textView.scrollEnabled = NO;
_textView.font = CELL_REG_FONT;
_textView.textContainer.lineFragmentPadding = 0.0;
_textView.textContainerInset = UIEdgeInsetsZero;
[self.contentView addSubview:_textView];
[self.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[view]-|" options:nil metrics:nil views:#{#"view": _textView}]];
[self.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[view]-|" options:nil metrics:nil views:#{#"view": _textView}]];
_heightConstraint = [NSLayoutConstraint constraintWithItem: _textView
attribute: NSLayoutAttributeHeight
relatedBy: NSLayoutRelationGreaterThanOrEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 0.0
constant: (_textView.font.lineHeight + 15)];
_heightConstraint.priority = UILayoutPriorityRequired - 1;
[_textView addConstraint:_heightConstraint];
}
return self;
}
- (void)prepareForReuse {
[super prepareForReuse];
self.minLines = 1;
}
- (void)setMinLines:(NSInteger)minLines {
_heightConstraint.constant = minLines * _textView.font.lineHeight + 15;
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
if ([self.delegate respondsToSelector:#selector(textInputTableViewCellTextWillChange:)]) {
[self.delegate textInputTableViewCellTextWillChange:self];
}
return YES;
}
- (void)textViewDidChange:(UITextView *)textView {
if ([self.delegate respondsToSelector:#selector(textInputTableViewCellTextDidChange:)]) {
[self.delegate textInputTableViewCellTextDidChange:self];
}
}
Next, we use it in the TableViewController
#interface SomeTableViewController () <TextInputTableViewCellDelegate>
#end
#implementation SomeTableViewController
. . . . . . . . . . . . . . . . . . . .
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
TextInputTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: TextInputTableViewCellIdentifier forIndexPath:indexPath];
cell.delegate = self;
cell.minLines = 3;
. . . . . . . . . .
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (void)textInputTableViewCellWillChange:(TextInputTableViewCell *)cell {
cell.lastRelativeFrameOriginY = cell.frame.origin.y - self.tableView.contentOffset.y;
}
- (void)textInputTableViewCellTextDidChange:(TextInputTableViewCell *)cell {
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
[UIView performWithoutAnimation:^{
[self.tableView moveRowAtIndexPath:indexPath toIndexPath:indexPath];
}];
CGFloat contentOffsetY = cell.frame.origin.y - cell.lastRelativeFrameOriginY;
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, contentOffsetY);
CGRect caretRect = [cell.textView caretRectForPosition:cell.textView.selectedTextRange.start];
caretRect = [self.tableView convertRect:caretRect fromView:cell.textView];
CGRect visibleRect = self.tableView.bounds;
visibleRect.origin.y += self.tableView.contentInset.top;
visibleRect.size.height -= self.tableView.contentInset.top + self.tableView.contentInset.bottom;
BOOL res = CGRectContainsRect(visibleRect, caretRect);
if (!res) {
caretRect.size.height += 5;
[self.tableView scrollRectToVisible:caretRect animated:NO];
}
}
#end
Here minLines allows to set minimum height for the textView (to
resist height minimizing by AutoLayout with
UITableViewAutomaticDimension).
moveRowAtIndexPath:indexPath: with the same indexPath starts
tableViewCell height re-calculation and re-layout.
performWithoutAnimation: removes side-effect (tableView content
offset jumping on starting new line while typing).
It is important to preserve relativeFrameOriginY (not
contentOffsetY!) during cell update because contentSize of the
cells before the current cell could be change by autoLayout calculus
in unexpected way. It removes visual jumps on system hyphenation
while typing long words.
Note that you shouldn't set the property estimatedRowHeight! The
following doesn't work
self.tableView.estimatedRowHeight = UITableViewAutomaticDimension;
Use only tableViewDelegate method.
==========================================================================
If one doesn't mind against weak binding between tableView and tableViewCell and updating geometry of the tableView from tableViewCell, it is possible to upgrade TextInputTableViewCell class above:
#interface TextInputTableViewCell : UITableViewCell
#property (nonatomic, weak) id<TextInputTableViewCellDelegate> delegate;
#property (nonatomic, weak) UITableView *tableView;
#property (nonatomic, readonly) UITextView *textView;
#property (nonatomic) NSInteger minLines;
#end
#import "TextInputTableViewCell.h"
#interface TextInputTableViewCell () <UITextViewDelegate> {
NSLayoutConstraint *_heightConstraint;
CGFloat _lastRelativeFrameOriginY;
}
#property (nonatomic) UITextView *textView;
#end
#implementation TextInputTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.selectionStyle = UITableViewCellSelectionStyleNone;
_textView = [UITextView new];
_textView.translatesAutoresizingMaskIntoConstraints = NO;
_textView.delegate = self;
_textView.scrollEnabled = NO;
_textView.font = CELL_REG_FONT;
_textView.textContainer.lineFragmentPadding = 0.0;
_textView.textContainerInset = UIEdgeInsetsZero;
[self.contentView addSubview:_textView];
[self.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"H:|-[view]-|" options:nil metrics:nil views:#{#"view": _textView}]];
[self.contentView addConstraints: [NSLayoutConstraint constraintsWithVisualFormat:#"V:|-[view]-|" options:nil metrics:nil views:#{#"view": _textView}]];
_heightConstraint = [NSLayoutConstraint constraintWithItem: _textView
attribute: NSLayoutAttributeHeight
relatedBy: NSLayoutRelationGreaterThanOrEqual
toItem: nil
attribute: NSLayoutAttributeNotAnAttribute
multiplier: 0.0
constant: (_textView.font.lineHeight + 15)];
_heightConstraint.priority = UILayoutPriorityRequired - 1;
[_textView addConstraint:_heightConstraint];
}
return self;
}
- (void)prepareForReuse {
[super prepareForReuse];
self.minLines = 1;
self.tableView = nil;
}
- (void)setMinLines:(NSInteger)minLines {
_heightConstraint.constant = minLines * _textView.font.lineHeight + 15;
}
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
_lastRelativeFrameOriginY = self.frame.origin.y - self.tableView.contentOffset.y;
return YES;
}
- (void)textViewDidChange:(UITextView *)textView {
NSIndexPath *indexPath = [self.tableView indexPathForCell:self];
if (indexPath == nil) return;
[UIView performWithoutAnimation:^{
[self.tableView moveRowAtIndexPath:indexPath toIndexPath:indexPath];
}];
CGFloat contentOffsetY = self.frame.origin.y - _lastRelativeFrameOriginY;
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, contentOffsetY);
CGRect caretRect = [self.textView caretRectForPosition:self.textView.selectedTextRange.start];
caretRect = [self.tableView convertRect:caretRect fromView:self.textView];
CGRect visibleRect = self.tableView.bounds;
visibleRect.origin.y += self.tableView.contentInset.top;
visibleRect.size.height -= self.tableView.contentInset.top + self.tableView.contentInset.bottom;
BOOL res = CGRectContainsRect(visibleRect, caretRect);
if (!res) {
caretRect.size.height += 5;
[self.tableView scrollRectToVisible:caretRect animated:NO];
}
}
#end

Put UILabel behind your UITextView.
Use this answer: https://stackoverflow.com/a/36054679/6681462 to UILabel you created
Give them same constraints and fonts
Set them same text;
Your cell's height will calculate by UILabel's content, but all text will be showed by TextField.

UITextView *txtDescLandscape=[[UITextView alloc] initWithFrame:CGRectMake(2,20,310,2)];
txtDescLandscape.editable =NO;
txtDescLandscape.textAlignment =UITextAlignmentLeft;
[txtDescLandscape setFont:[UIFont fontWithName:#"ArialMT" size:15]];
txtDescLandscape.text =[objImage valueForKey:#"imgdescription"];
txtDescLandscape.text =[txtDescLandscape.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
[txtDescLandscape sizeToFit];
[headerView addSubview:txtDescLandscape];
CGRect txtViewlandscpframe = txtDescLandscape.frame;
txtViewlandscpframe.size.height = txtDescLandscape.contentSize.height;
txtDescLandscape.frame = txtViewlandscpframe;
i think this way you can count the height of your text view and then resize your tableview cell according to that height so that you can show full text on cell

Swift version
func textViewHeightForAttributedText(text: NSAttributedString, andWidth width: CGFloat) -> CGFloat {
let calculationView = UITextView()
calculationView.attributedText = text
let size = calculationView.sizeThatFits(CGSize(width: width, height: CGFloat.max))
return size.height
}

If you want to automatically adjust UITableViewCell's height based on the height of the inner UITextView's height. See my answer here: https://stackoverflow.com/a/45890087/1245231
The solution is quite simple and should work since iOS 7. Make sure that the Scrolling Enabled option is turned off for the UITextView inside the UITableViewCell in the StoryBoard.
Then in your UITableViewController's viewDidLoad() set the tableView.rowHeight = UITableViewAutomaticDimension and tableView.estimatedRowHeight > 0 such as:
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 44.0
}
That's it. UITableViewCell's height will be automatically adjusted based on the inner UITextView's height.

For iOS 8 and above you can just use
your_tablview.estimatedrowheight= minheight you want
your_tableview.rowheight=UItableviewautomaticDimension

Related

Not getting UIImageView Rounded Border while Added Multiplier for UIImageViwe

In Storyboard I applied Multiplier for height and width to UIImageView then I just want to rounded border so I used below code its not work for all iPhones.
_profileImgView.clipsToBounds = YES;
_profileImgView.layer.backgroundColor = color.CGColor;
_profileImgView.layer.cornerRadius =_profileImgView.frame.size.width/2;
_profileImgView.layer.borderColor = [UIColor colorWithRed:253.0/255.0 green:182.0/255.0 blue:43.0/255.0 alpha:100].CGColor;
_profileImgView.layer.borderWidth = 5.0f;
Since your corner radius depends on your frame size, you need to update it whenever the frame size changes. If you are using storyboards for your design, you will get the frame size that is in the design when viewDidLoad is called. If the frame size differs for different devices, you will get the final size at a later point in time in the views layoutSubviews or possibly the view controller's viewDidLayoutSubviews.
My suggested solution is to sub-class UIImageView and put the specifics for the image view in awakeFromNib and layoutSubviews, then use this class instead of UIImageView where appropriate.
// CircularImageView.h
#import <UIKit/UIKit.h>
#interface CircularImageView : UIImageView
#end
// CircularImageView.m
#implementation CircularImageView
- (void)awakeFromNib {
[super awakeFromNib];
self.clipsToBounds = YES;
self.layer.borderColor = [UIColor colorWithRed:253.0/255.0 green:182.0/255.0 blue:43.0/255.0 alpha:100].CGColor;
self.layer.borderWidth = 5.0f;
}
- (void)layoutSubviews {
[super layoutSubviews];
self.layer.cornerRadius = self.frame.size.width / 2;
}
#end
implement this code in same Viewcontroller class it is work for me
-(void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
_profileImgView.clipsToBounds = YES;
_profileImgView.layer.backgroundColor = color.CGColor;
_profileImgView.layer.cornerRadius =_profileImgView.frame.size.width/2;
_profileImgView.layer.borderColor = [UIColor colorWithRed:253.0/255.0 green:182.0/255.0 blue:43.0/255.0 alpha:100].CGColor;
_profileImgView.layer.borderWidth = 5.0f;
}

How to make UITextView in section header adjust its height to its content

I cannot get this to work. I am using autolayout on the current view controller. I have a UITableView that has section headers and each section header has UITextView that has text that varies in length depending on the section. I cannot make it enlarge its height automatically to fit the contents so there will be no need for scroll (its contents are attributed text)
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
//UITextView *tv = [[UITextView alloc] init];
//tv.editable = NO;
//tv.attributedText = [self millionaireResults][section][#"header"];
//return tv;
return [self millionaireResults][section][#"headerview"]; //this is a uitextview
}
// this did not workeither
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
UITextView *tv = [self millionaireResults][section][#"headerview"];
return tv.frame.size.height;
}
How can this problem be solved?
I updated the code per the suggestion of Michael below
Make your "UITextView *tv" object a property and then you can do something like this (assuming you only have exactly one section to your table view):
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
return (self.tv.frame.size.height);
}
If you have more sections (which is appears you do), you should make that property a NSArray of UITextView objects.
This also means you need to set the contents of your "tv" object before "viewForHeaderInSection:" gets called.
This is the answer that worked for me
When you are creating the UITextView, you must set the scrollEnabled
to false.
Your UITextView must be given the width that covers horizontal space otherwise auto size calculation are off (sometimes it is sometimes it is not, i think depending on wordbreak or something, but it was inconsistent!) and only fixes itself if you rotate the device to force redraw
In the heightForHeaderInSection method, you must get the
sizeThatFits and return its height as the height of your text view
Here is the height calculation (I found this on this site http://www.raywenderlich.com/50151/text-kit-tutorial )
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
UITextView *tv1 = (UITextView *)[self millionaireResults][section][#"headerview"];
// sizethatfits calculates the perfect size for this UITextView
// if you gave your UITextView full width
CGSize goodsize = [tv1 sizeThatFits:tv1.frame.size];
return goodsize.height+4; // here 4 is not necessary, i just put it as an offset
}
Here is the code that creates those UITextView objects
for (int i = 0; i < [milarr count]; i++) {
UITextView *tv = [[UITextView alloc] init];
tv.editable = NO;
tv.attributedText = milarr[i];
// labelTopTitle and this table in question have same width in an autoLayouted view
// so i am giving width of labelTopTitle to let UITextView cover full available
// horizontal space
tv.frame = CGRectMake(0, 0, self.labelTopTitle.frame.size.width,FLT_MAX);
//tv.backgroundColor = [UIColor grayColor];
//tv.textContainerInset = UIEdgeInsetsZero;
tv.scrollEnabled = NO;
[results addObject:#{#"headerview": tv,
#"rows":#[...]
}
];
}

drawBackgroundInRect not called in layer backed view

I have an NSScrollView which is set to be layer backed by clicking the layer checkmark on the scrollview in IB. Within that scrollview I have an NSTableView. In that NSTableView I use a custom NSTableRowView to draw a vertical red line at the divider between my columns. My NSTableView has 2 columns. With my scrollview set to be layer backed, whenever I drag the column divider to make the column wider or narrower, drawBackgroundInRect is not being called, so my background vertical line does not get updated during the drag operation.
Is there a workaround for this?
Here's the code I'm using in drawBackgroundInRect:
- (void)drawBackgroundInRect:(NSRect)dirtyRect {
[super drawBackgroundInRect:dirtyRect];
NSGraphicsContext *nscontext = [NSGraphicsContext currentContext];
[nscontext saveGraphicsState];
CGContextRef context = (CGContextRef)[nscontext graphicsPort];
if (!self.isGroupRowStyle) {
CGRect leftColumnRect = [(NSView *)[self viewAtColumn:0] frame];
leftColumnRect.origin.y -= 1.0;
leftColumnRect.size.height += 2.0;
leftColumnRect.size.width += 1.0;
// grey background
CGContextSetGrayFillColor(context, 0.98, 1.0);
CGContextFillRect(context, leftColumnRect);
// draw nice red vertical line
CGContextBeginPath(context);
CGContextSetRGBStrokeColor(context, 0.6, 0.2, 0.2, 0.3);
CGContextMoveToPoint(context, leftColumnRect.size.width + 1.5, 0);
CGContextSetLineWidth(context, 1.0);
CGContextAddLineToPoint(context, leftColumnRect.size.width + 1.5, leftColumnRect.size.height);
CGContextStrokePath(context);
}
[nscontext restoreGraphicsState];
}
Here's the problem that I'm getting when resizing the column:
And this is what it should look like when I don't have my scrollview set to be layer backed:
Thanks!
After more than a year, I finally figured out a proper answer to my own question.
The trick is to call setNeedsDisplay on every row in the list of visible rows as the NSTableColumn is being resized.
Here's the code I use in my NSViewController subclass:
- (void)tableView:(NSTableView *)tableView isResizingTableColumn:(NSTableColumn *)tableColumn {
NSRange range = [tableView rowsInRect:tableView.visibleRect];
for (NSInteger row = range.location; row < (range.location + range.length); row++) {
NSTableRowView *rowView = [tableView rowViewAtRow:row makeIfNecessary:NO];
[rowView setNeedsDisplay:YES];
}
}
My NSTableView has as its header view my own subclass of NSTableHeaderView. I set my NSViewController as the delegate for the header view so I can track when the user resizes.
TFRecordTableHeaderView.h:
#import <Cocoa/Cocoa.h>
#protocol TFRecordTableHeaderViewDelegate <NSObject>
- (void)tableView:(NSTableView *)tableView isResizingTableColumn:(NSTableColumn *)tableColumn;
#end
#interface TFRecordTableHeaderView : NSTableHeaderView
#property (nonatomic, assign) id<TFRecordTableHeaderViewDelegate>delegate;
#end
TFRecordTableHeaderView.m:
#import "TFRecordTableHeaderView.h"
#implementation TFRecordTableHeaderView
- (id)initWithFrame:(NSRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// do some custom drawing here
}
// track resizing of the column as it happens
// from the docs: If the user is resizing a column in the receiver, returns the index of that column.
- (NSInteger)resizedColumn {
NSInteger columnIndex = [super resizedColumn];
if (columnIndex >= 0) {
NSTableColumn *column = [self.tableView.tableColumns objectAtIndex:columnIndex];
if ([self.delegate respondsToSelector:#selector(tableView:isResizingTableColumn:)]) {
[self.delegate tableView:self.tableView isResizingTableColumn:column];
}
}
return columnIndex;
}
#end
Just set the NSTableHeaderView subclass to your NSTableView's header view. Now whenever you drag a column in the header, the isResizingTableColumn delegate method will be called. You can implement the isResizingTableColumn on your NSViewController subclass.
Now when you resize a column, isResizingTableColumn will be called which will get the NSTableRowViews for the visible rect and it will send setNeedsDisplay. That will cause the rows to be refreshed and the drawBackgroundInRect method to get called while dragging. This in turn will refresh my custom vertical grid lines.
All this to avoid drawing a vertical grid line over top grouped rows on an NSTableView. I think this should be built-in to NSTableView. It looks really bad if your group section headers have text in them and there's a vertical grid line running right over top the text.
Now that I think about it, you could just do this right on your NSTableHeaderView subclass:
- (NSInteger)resizedColumn {
NSInteger columnIndex = [super resizedColumn];
if (columnIndex >= 0) {
NSRange range = [self.tableView rowsInRect:self.tableView.visibleRect];
for (NSInteger row = range.location; row < (range.location + range.length); row++) {
NSTableRowView *rowView = [self.tableView rowViewAtRow:row makeIfNecessary:NO];
[rowView setNeedsDisplay:YES];
}
}
return columnIndex;
}
In my case I was doing it on my NSViewController subclass because while I resized one table column I was also resizing another equally. This was simulating a footer row with another similarly configured table that had only one row in it since NSTableView doesn't have the concept of a footer like UITableView does.

How do I size a UITextView to its content on iOS 7?

I've been using the accepted answer here for years.
On iOS 7, the contentSize.height becomes the frame.height-8, regardless of text content.
What's a working method to adjust the height on iOS 7?
I favor this minimal code change: Just add these two lines after addSubview and before grabbing the height of the frame
...
[scrollView1 addSubview: myTextView];
[myTextView sizeToFit]; //added
[myTextView layoutIfNeeded]; //added
CGRect frame = myTextView.frame;
...
This is tested backwards compatible with iOS 6. NOTE that it shrink-wraps the width. If you're just interested in the height and have a fixed width, just grab the new height but set the original width, and it works just as before on both iOS 6 and 7.
(Speculation: it does size to fit on iOS 7 as well, but the layout is updated later or in a separate thread, and that this forces the layout immediately so that its frame is updated in time for using its height value a few lines later in the same thread.)
NOTES:
1) You might or might not have implemented the outer container resize this way. It does seem to be a common snippet, though, and I've used it in my projects.
2) Since sizeToFit seems to work as expected on iOS 7, you likely don't need the premature addSubView. Whether it will still work on iOS 6 then is untested by me.
3) Speculation: The extra layoutIfNeeded mid-thread might be costly. The alternative as I see it is to resize the outer container on the layout callback (fired or not depending on if the OS decides whether layout is needed or not) where the outer container resize will cause another layout update. Both updates might be combined with other layout updates to be more efficient. If you do have such a solution and you can show that it is more efficient, add it as answer and I'll be sure to mention it here.
Since I'm using Auto Layout, I use the value of [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)].height to update the constant of the textView's height UILayoutConstraint.
I use an adapted version of madmik's answer that eliminates the fudge factor:
- (CGFloat)measureHeightOfUITextView:(UITextView *)textView
{
if ([textView respondsToSelector:#selector(snapshotViewAfterScreenUpdates:)])
{
// This is the code for iOS 7. contentSize no longer returns the correct value, so
// we have to calculate it.
//
// This is partly borrowed from HPGrowingTextView, but I've replaced the
// magic fudge factors with the calculated values (having worked out where
// they came from)
CGRect frame = textView.bounds;
// Take account of the padding added around the text.
UIEdgeInsets textContainerInsets = textView.textContainerInset;
UIEdgeInsets contentInsets = textView.contentInset;
CGFloat leftRightPadding = textContainerInsets.left + textContainerInsets.right + textView.textContainer.lineFragmentPadding * 2 + contentInsets.left + contentInsets.right;
CGFloat topBottomPadding = textContainerInsets.top + textContainerInsets.bottom + contentInsets.top + contentInsets.bottom;
frame.size.width -= leftRightPadding;
frame.size.height -= topBottomPadding;
NSString *textToMeasure = textView.text;
if ([textToMeasure hasSuffix:#"\n"])
{
textToMeasure = [NSString stringWithFormat:#"%#-", textView.text];
}
// NSString class method: boundingRectWithSize:options:attributes:context is
// available only on ios7.0 sdk.
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
[paragraphStyle setLineBreakMode:NSLineBreakByWordWrapping];
NSDictionary *attributes = #{ NSFontAttributeName: textView.font, NSParagraphStyleAttributeName : paragraphStyle };
CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attributes
context:nil];
CGFloat measuredHeight = ceilf(CGRectGetHeight(size) + topBottomPadding);
return measuredHeight;
}
else
{
return textView.contentSize.height;
}
}
Based on other answers, I made it work(in Swift).
This solves the problem with newline character.
textView.sizeToFit()
textView.layoutIfNeeded()
let height = textView.sizeThatFits(CGSizeMake(textView.frame.size.width, CGFloat.max)).height
textView.contentSize.height = height
Auto Layout is needed.
If you're using Auto Layout, you could create a trivial UITextView subclass that self-sizes the text view height to fit the content:
#interface ContentHeightTextView : UITextView
#end
#interface ContentHeightTextView ()
#property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
#end
#implementation ContentHeightTextView
- (void)layoutSubviews
{
[super layoutSubviews];
CGSize size = [self sizeThatFits:CGSizeMake(self.bounds.size.width, FLT_MAX)];
if (!self.heightConstraint) {
self.heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0f constant:size.height];
[self addConstraint:self.heightConstraint];
}
self.heightConstraint.constant = size.height;
[super layoutSubviews];
}
#end
Of course, the text view's width and position must be defined by additional constraints configured elsewhere in the program.
If you create this custom text view in IB, give the text view a height constraint in order to satisfy Xcode; just make sure the height constraint created in IB is merely a placeholder (i.e., tick the box that says "Remove at build time").
An alternative way to implement the UITextView subclass is as follows (this implementation might qualify as best practice):
#interface ContentHeightTextView ()
#property (nonatomic, strong) NSLayoutConstraint *heightConstraint;
#end
#implementation ContentHeightTextView
- (void)layoutSubviews
{
[super layoutSubviews];
[self setNeedsUpdateConstraints];
}
- (void)updateConstraints
{
CGSize size = [self sizeThatFits:CGSizeMake(self.bounds.size.width, FLT_MAX)];
if (!self.heightConstraint) {
self.heightConstraint = [NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:0 multiplier:1.0f constant:size.height];
[self addConstraint:self.heightConstraint];
}
self.heightConstraint.constant = size.height;
[super updateConstraints];
}
#end
If you are using auto-layout, you can use the following UITextView subclass that adds an intrinsic height:
#implementation SelfSizingTextView
- (void)setText:(NSString *)text
{
[super setText:text];
[self invalidateIntrinsicContentSize];
}
- (void)setFont:(UIFont *)font
{
[super setFont:font];
[self invalidateIntrinsicContentSize];
}
- (CGSize)intrinsicContentSize
{
CGFloat width = self.frame.size.width;
CGSize size = [self sizeThatFits:CGSizeMake(width, MAXFLOAT)];
return CGSizeMake(UIViewNoIntrinsicMetric, size.height);
}
#end
this method seems to work.
// Code from apple developer forum - #Steve Krulewitz, #Mark Marszal, #Eric Silverberg
- (CGFloat)measureHeight
{
if ([self respondsToSelector:#selector(snapshotViewAfterScreenUpdates:)])
{
CGRect frame = internalTextView.bounds;
CGSize fudgeFactor;
// The padding added around the text on iOS6 and iOS7 is different.
fudgeFactor = CGSizeMake(10.0, 16.0);
frame.size.height -= fudgeFactor.height;
frame.size.width -= fudgeFactor.width;
NSMutableAttributedString* textToMeasure;
if(internalTextView.attributedText && internalTextView.attributedText.length > 0){
textToMeasure = [[NSMutableAttributedString alloc] initWithAttributedString:internalTextView.attributedText];
}
else{
textToMeasure = [[NSMutableAttributedString alloc] initWithString:internalTextView.text];
[textToMeasure addAttribute:NSFontAttributeName value:internalTextView.font range:NSMakeRange(0, textToMeasure.length)];
}
if ([textToMeasure.string hasSuffix:#"\n"])
{
[textToMeasure appendAttributedString:[[NSAttributedString alloc] initWithString:#"-" attributes:#{NSFontAttributeName: internalTextView.font}]];
}
// NSAttributedString class method: boundingRectWithSize:options:context is
// available only on ios7.0 sdk.
CGRect size = [textToMeasure boundingRectWithSize:CGSizeMake(CGRectGetWidth(frame), MAXFLOAT)
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
return CGRectGetHeight(size) + fudgeFactor.height;
}
else
{
return self.internalTextView.contentSize.height;
}
}
If you're using iOS 7+, you can just turn on auto layout, pin each of the sides of the text view to the edge of its parent view, and it works fine. No additional code needed.
Not sure if this was always the case but the following is true since at least iOS 10.
UITextView implements the intrinsicContentSize property if scrollEnabled == NO. That means you just need to make sure the width of the text view is constrained enough and then you can use the intrinsic content height (either via Auto Layout content hugging/compression resistance priorities or directly using the value during manual layout).
Unfortunately, this behavior is not documented. Apple could have easily saved us all some headaches… no need for an extra height constraint, subclassing, etc.
In iOS 8 you'll in inherit some content offset from the parent, which you need to get rid of as well.
A subclass example
// Originally from https://github.com/Nikita2k/resizableTextView
#import "ResizableTextView.h"
#implementation ResizableTextView
- (void) updateConstraints {
// calculate contentSize manually
// ios7 doesn't calculate it before viewDidAppear and we'll get here before
CGSize contentSize = [self sizeThatFits:CGSizeMake(self.frame.size.width, FLT_MAX)];
// set the height constraint to change textView height
[self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
if (constraint.firstAttribute == NSLayoutAttributeHeight) {
constraint.constant = contentSize.height;
*stop = YES;
}
}];
[super updateConstraints];
}
- (void)setContentOffset:(CGPoint)contentOffset
{
// In iOS 8 we seem to be inheriting the content offset from the parent.
// I'm not interested
}
#end
In storyboard, if using constraints, make sure you are constrained to your superview in the 'ruler' tab of the right-hand pane on xcode for the UITextView. My problem was that I had a constraint of -80 pts on the 'Trailing space to'.
Guys using autolayout and your sizetofit isn't working, then please check your width constraint once. If you had missed the width constraint then the height will be accurate.
No need to use any other API. just one line would fix all the issue.
[_textView sizeToFit];
Here, I was only concerned with height, keeping the width fixed and had missed the width constraint of my TextView in storyboard.
And this was to show up the dynamic content from the services.
Hope this might help..
I wrote a category over UITextView:
- (CGSize)intrinsicContentSize {
return self.contentSize;
}
- (void)setContentSize:(CGSize)contentSize {
[super setContentSize:contentSize];
[self invalidateIntrinsicContentSize];
}
When UIKit sets its contentSize, UITextView adjusts its intrinsic content size. That plays nicely with autolayout.
The answer given by bilobatum worked perfectly With auto layout, i.e subclassing the textview.
If you want to limit the height of the text view add another constraint (I added it using storyboard i.e. height <= 166 (height as per your need))
Then inside subclass reduce the priority of height constraint to 750 (self.heightConstraint.priority = 750) to avoid conflict between height constraint added in subclass and height constraint added on storyboard.

Content padding in custom view with Cocoa auto layout

I have a custom NSView subclass which has a border around itself. The border is drawn inside this view. Is it possible to respect this borders with auto layout?
For example, when I place the subview to my custom view and set constraints like this:
#"H:|-(myViewSubView)-|" (not #"H:|-(myViewBorderWidth)-(myViewSubView)-(myViewBorderWidth)-|")
#"V:|-(myViewSubView)-|"
the layout must be:
Horizontal: |-(myViewBorderWidth)-|myViewSubview|-(myViewBorderWidth)-|
Vertical: |-(myViewBorderWidth)-|myViewSubview|-(myViewBorderWidth)-|
I've tried to overwrite -bounds method in my view to return the bounds rect without the borders, but it doesn't help.
UPDATE
I just noticed that your question is talking about NSView (OS X), not UIView (iOS). Well, this idea should still be applicable, but you won't be able to drop my code into your project unchanged. Sorry.
ORIGINAL
Consider changing your view hierarchy. Let's say your custom bordered view is called BorderView. Right now you're adding subviews directly to BorderView and creating constraints between the BorderView and its subviews.
Instead, give the BorderView a single subview, which it exposes in its contentView property. Add your subviews to the contentView instead of directly to the BorderView. Then the BorderView can lay out its contentView however it needs to. This is how UITableViewCell works.
Here's an example:
#interface BorderView : UIView
#property (nonatomic, strong) IBOutlet UIView *contentView;
#property (nonatomic) UIEdgeInsets borderSize;
#end
If we're using a xib, then we have the problem that IB doesn't know that it should add subviews to the contentView instead of directly to the BorderView. (It does know this for UITableViewCell.) To work around that, I've made contentView an outlet. That way, we can create a separate, top-level view to use as the content view, and connect it to the BorderView's contentView outlet.
To implement BorderView this way, we'll need an instance variable for each of the four constraints between the BorderView and its contentView:
#implementation BorderView {
NSLayoutConstraint *topConstraint;
NSLayoutConstraint *leftConstraint;
NSLayoutConstraint *bottomConstraint;
NSLayoutConstraint *rightConstraint;
UIView *_contentView;
}
The contentView accessor can create the content view on demand:
#pragma mark - Public API
- (UIView *)contentView {
if (!_contentView) {
[self createContentView];
}
return _contentView;
}
And the setter can replace an existing content view, if there is one:
- (void)setContentView:(UIView *)contentView {
if (_contentView) {
[self destroyContentView];
}
_contentView = contentView;
[self addSubview:contentView];
}
The borderSize setter needs to arrange for the constraints to be updated and for the border to be redrawn:
- (void)setBorderSize:(UIEdgeInsets)borderSize {
if (!UIEdgeInsetsEqualToEdgeInsets(borderSize, _borderSize)) {
_borderSize = borderSize;
[self setNeedsUpdateConstraints];
[self setNeedsDisplay];
}
}
We'll need to draw the border in drawRect:. I'll just fill it with red:
- (void)drawRect:(CGRect)rect {
CGRect bounds = self.bounds;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:bounds];
[path appendPath:[UIBezierPath bezierPathWithRect:UIEdgeInsetsInsetRect(bounds, self.borderSize)]];
path.usesEvenOddFillRule = YES;
[path addClip];
[[UIColor redColor] setFill];
UIRectFill(bounds);
}
Creating the content view is trivial:
- (void)createContentView {
_contentView = [[UIView alloc] init];
[self addSubview:_contentView];
}
Destroying it is slightly more involved:
- (void)destroyContentView {
[_contentView removeFromSuperview];
_contentView = nil;
[self removeConstraint:topConstraint];
topConstraint = nil;
[self removeConstraint:leftConstraint];
leftConstraint = nil;
[self removeConstraint:bottomConstraint];
bottomConstraint = nil;
[self removeConstraint:rightConstraint];
rightConstraint = nil;
}
The system will automatically call updateConstraints before doing layout and drawing if somebody has called setNeedsUpdateConstraints, which we did in setBorderSize:. In updateConstraints, we'll create the constraints if necessary, and update their constants based on borderSize. We also tell the system not to translate the autoresizing masks into constraints, because that tends to create unsatisfiable constraints.
- (void)updateConstraints {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
[super updateConstraints];
if (!topConstraint) {
[self createContentViewConstraints];
}
topConstraint.constant = _borderSize.top;
leftConstraint.constant = _borderSize.left;
bottomConstraint.constant = -_borderSize.bottom;
rightConstraint.constant = -_borderSize.right;
}
All four constraints are created the same way, so we'll use a helper method:
- (void)createContentViewConstraints {
topConstraint = [self constrainContentViewAttribute:NSLayoutAttributeTop];
leftConstraint = [self constrainContentViewAttribute:NSLayoutAttributeLeft];
bottomConstraint = [self constrainContentViewAttribute:NSLayoutAttributeBottom];
rightConstraint = [self constrainContentViewAttribute:NSLayoutAttributeRight];
}
- (NSLayoutConstraint *)constrainContentViewAttribute:(NSLayoutAttribute)attribute {
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem:_contentView attribute:attribute relatedBy:NSLayoutRelationEqual toItem:self attribute:attribute multiplier:1 constant:0];
[self addConstraint:constraint];
return constraint;
}
#end
I have put a complete working example in this git repository.
For future reference, you can override NSView.alignmentRectInsets to affect the position of the layout guides:
Custom views that draw ornamentation around their content can override
this property and return insets that align with the edges of the
content, excluding the ornamentation. This allows the constraint-based
layout system to align views based on their content, rather than just
their frame.
Link to documentation:
https://developer.apple.com/documentation/appkit/nsview/1526870-alignmentrectinsets
Have you tried setting the intrinsic size to include the border size?
- (NSSize)intrinsicContentSize
{
return NSMakeSize(width+bordersize, height+bordersize);
}
then you would set the content compression resistance priorities in both directions to be required:
[self setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintHorizontal];
[self setContentCompressionResistancePriority:NSLayoutPriorityRequired forOrientation:NSLayoutConstraintVertical];
The one solution I found is to overload the addConstraint: method and modify constraints before they'll be added:
- (void)addConstraint:(NSLayoutConstraint *)constraint
{
if(constraint.firstItem == self || constraint.secondItem == self) {
if(constraint.firstAttribute == NSLayoutAttributeLeading) {
constraint.constant += self.leftBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeTrailing) {
constraint.constant += self.rightBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeTop) {
constraint.constant += self.topBorderWidth;
} else if (constraint.firstAttribute == NSLayoutAttributeBottom) {
constraint.constant += self.bottomBorderWidth;
}
}
[super addConstraint:constraint];
}
And then also handle this constraints in xxxBorderWidth setters.