Multi-line/Dynamic UILabel Truncating in iOS8 - ios

I am using UILabels with numberOfLines=0 and lineBreakMode = NSLineBreakByWordWrapping in a "dynamic" UITableViewCell . I utilize AutoLayout to attach the label to its container cell at the top, bottom, trailing and leading edges. In iOS7, the label's height updates dynamically with its content, however in iOS8 it truncates prematurely (usually at 1 line, but sometimes at 2). In iOS8, I have confirmed that the intrinsicContentSize of the label is dynamically updating as expected, but the label's frame is not following suit. Here is the difference seen between iOS7 and iOS8:
I have read of similar bugs in iOS8, but have been unable to implement a workaround. I have tried setting the label's text to #"" and back to #"Abraham Lincoln: Vampire Hunter" (as mentioned in this question), but that does not remedy the issue. Also notice that in iOS8, the cell size does increase according to the label's intrinsicContentSize, but for whatever reason, the height of the label itself does not. Any suggestions would be greatly appreciated. I would be glad to provide more information as well.
iOS 8
iOS 7

This is my walk around
subclass of UILabel, and override this function.
-(CGRect)textRectForBounds:(CGRect)bounds
limitedToNumberOfLines:(NSInteger)numberOfLines
{
CGRect rect = [super textRectForBounds:bounds
limitedToNumberOfLines:numberOfLines];
float height = rect.size.height;
int hightRound = (int)roundf(height);
rect.size.height = hightRound;
return rect;
}

Related

iOS storyboard - Auto Layout won't resize and reposition subviews

I don't want to set a Y position constraint because I need the views to be relative to each other, since I have a UITextView that has to and should dynamically change its value based on how many lines of text are in it. It does not do this though. I can FORCE it to do it by calling sizeToFit() on the UITextView, but then it overlaps the views below.
Xcode nags me to reduce ambiguity and there are red lines all over the view controller, but it doesn't make any sense for me to manually have a Y position constraint if that has to be dynamic.
So my question is how do I
1. Make my UITextView resize its height after the number of lines of text increases
2. Make the views below it get pushed down automatically so the UITextView does not overlap them.
I've read multiple answers and have managed to do 1 but not the other.
Solution in Swift:
let sizeThatFitsTextView = textView.sizeThatFits(CGSizeMake(textView.frame.size.width, textView.frame.size.height))
textViewHeightConstraint.constant = sizeThatFitsTextView.height
1) Disable the scroll of Textview from storyboard or by coding.
2) Create an outlet for textview's height constraint. Change the height constraint programmatically from textView delegate method when text change occur based on calculation.
CGSize sizeThatFitsTextView = [TextView sizeThatFits:CGSizeMake(TextView.frame.size.width, MAXFLOAT)];
TextViewHeightConstraint.constant = sizeThatFitsTextView.height;

UITextView text becomes invisible after height reaches 8192.0

I have a non-scrollable UITextView embedded in a UIScrollView and add text to the UITextView dynamically. The UIScrollView adjust it's contentSize accordingly based on the TextView's frame. However, once the UITextView exceeds a height of 8192, the text will become invisible (but still there, because you can use the magnifying glass to highlight text and even see parts of the text through the magnifying glass).
CGRect textviewFrame = self.TextView.frame;
textviewFrame.size.height = [self textViewHeightForAttributedText:self.TextView.attributedText andWidth:320.0];
self.TextView.frame = textviewFrame;
self.ScrollView.contentSize = self.TextView.frame.size;
Helper function to size UITextView accordingly:
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString *)text andWidth:(CGFloat)width
{
UITextView *textView = [[UITextView alloc] init];
[textView setAttributedText:text];
CGSize size = [textView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
Didn't realize it was the same exact problem that was unsolved here until I tested it out explicitly by forcing the max size to 8193 and the problem occurred (while a max size of 8192 still had the text showing correctly). Anyone run into this problem before and know of a work around? Thanks
I was recently hit by this problem and have worked out an effective way around it. Although it seems like an iOS bug IMHO it's really not... there are practical limits to CALayer sizes, plus drawing an 8K high piece of text takes a long time. Much better to do as Apple intended and to only render the bit of text that's visible... that's why UITextView extends UIScrollView after all.
The problem is that UITextView isn't terribly easy to integrate with other bits of UI. In my case I am working on a news app where a single UITextView is used to render the article, plus there's some separate UI (titles and buttons etc) above and below it, all hosted in a single scrollable container.
The solution I've found is to split the UITextView into two views... a dedicated UITextView container whose frame is the full text size (i.e. the same size your UITextView's contentSize) and which is the superview of your UITextView. Your child UITextView's frame should be set to the bounds of the outer scrollable container.
Then all you have to do is use key-value observation to monitor the contentOffset property of your outer scrollable container view (in my case this is a UICollectionView). When its contentOffset changes you update (1) the contentOffset of your UITextView, and (2) the transform property of the UITextView's layer. By updating that transform property the UITextView is fixed to fill the currently-visible part of it's superview. But because you're also updating the UITextView's contentOffset, this trickery is totally invisible to the user... it looks and behaves as if the UITextView is simply very large.
Here's a fully functional solution, for anyone who'd like it!
** Assuming your content size will not exceed the limits of two text views **
This solution works by adding two UITextViews to your view, and splitting your text between them. It looks complicated, but it's actually very simple! I've just written a very verbose description :)
Step 1 - Add two UITextViews to your view:
I added mine in my storyboard. Place them so that one is directly above the other, with no space between them. Don't worry about setting the height (we will set that later).
Set constraints on the views so that they are tied to each other from the top and bottom, and the surrounding edges of the container from all other sides, including your desired padding. i.e. tie the first text view to the container from the top, left, and right, and to the second text view from the bottom. Tie the second text view to the container from the bottom, left, and right, and to the first text view from the top. This will ensure that they stretch appropriately when the content is set.
Don't set any constraints on the height of the views, or if you must (to avoid warnings from the constraints inspector), set the height of one of the views to be >= 20, or some similarly small number.
Disable scrolling, bouncing, and scrolling indicators for both your text views. This solution relies on the views being a fixed, non-scrollable height, so if you'd like your content to scroll, you should use a UIScrollView or UITableViewCell as a container.
Create outlets for your two new text views in your view controller file, naming them something like textView1 and textView2.
Step 2 - Set textContainerInsets to zero:
Set the textContainerInset property on both text views to zero, either using User Defined Runtime Attributes:
or code:
self.textView1.textContainerInset = UIEdgeInsetsZero;
self.textView2.textContainerInset = UIEdgeInsetsZero;
This will ensure that the no visible space will appear between the two views when the content is set, and should not affect the other spacing around your views.
Step 3 - Split your content, set it, and update the view heights:
Simply copy the following code into your view controller file (viewDidLoad), and set the contentString variable to your content.
/* Content is split across two UITextViews to avoid max drawing height */
NSString *contentString = #"Some very long piece of text...";
// Set text
NSArray *components = [contentString componentsSeparatedByString:#"\n"];
NSInteger halfLength = [components count] / 2;
NSArray *firstHalf = [components subarrayWithRange:NSMakeRange(0, halfLength)];
NSArray *secondHalf = [components subarrayWithRange:NSMakeRange(halfLength, [components count] - halfLength)];
NSString *contentString1 = [firstHalf componentsJoinedByString:#"\n"];
NSString *contentString2 = [secondHalf componentsJoinedByString:#"\n"];
self.textView1.text = contentString1;
self.textView2.text = contentString2;
// Set text view heights
CGFloat fixedWidth1 = self.textView1.frame.size.width;
CGFloat fixedWidth2 = self.textView2.frame.size.width;
CGSize newSize1 = [self.textView1 sizeThatFits:CGSizeMake(fixedWidth1, CGFLOAT_MAX)];
CGSize newSize2 = [self.textView2 sizeThatFits:CGSizeMake(fixedWidth2, CGFLOAT_MAX)];
CGRect newFrame1 = self.textView1.frame;
CGRect newFrame2 = self.textView2.frame;
newFrame1.size = CGSizeMake(fmaxf(newSize1.width, fixedWidth1), MIN(newSize1.height, 8192));
newFrame2.size = CGSizeMake(fmaxf(newSize2.width, fixedWidth2), MIN(newSize2.height, 8192));
self.textView1.frame = newFrame1;
self.textView2.frame = newFrame2;
This code splits the contentString roughly in the middle, looking for the nearest newline. If you'd like to split your content on a different character, simply change all occurrences of \n above to whatever you'd like to split on.
Step 4 - Set your container view height:
Set your container view (scrollView, tableViewCell, whatever else) to the height of your two text views, plus whatever additional space you've set above and below them.
CGRect viewFrame = self.myView.frame;
viewFrame.size.height = MIN(self.textView1.frame.size.height, 8192) + MIN(self.textView2.frame.size.height, 8192) + kTextViewContainerPadding;
[self.myView setFrame:viewFrame];
(In my code, kTextViewContainerPadding is a macro I've set to the sum of the space above and below my two text views, within their container).
That's it! Good luck.
Try enabling the scroll for the scrollView.
Keep the height of the textView > height of the content, so that in reality there will be no scroll, but scrollEnabled should be = YES
It solved the problem for me.
Hello I think am not late to answer. I got the same problem like you. This is my solution:
textView.scrollEnabled = YES;
contentTextView.attributedText = finalAttrString;
// contentTextView.text = [attrString string];
contentTextView.font = kFont(contentTextFS + [valueOfStepper intValue]);
[contentTextView sizeToFit];
contentTextView.height += 1;//This is the key code
//contentTextView.height = 8192.0f
Them I solved the trouble and I can change size dynamic.Successfull on iOS 8

UITextView Word Wrapping Unexpected behavior

I have a UITextView with Content and frame size widths equal to 348, but word wrapping happens whenever text width on a line exceeds 338.61. Does anyone know why this might be happening? How can I access the width that the UITextView uses for word wrapping dynamically?
As of iOS 7, yes. It's a combination of textContainerInset and lineFragmentPadding.
UITextView *textView = ...;
CGFloat wrappingWidth = textView.bounds.size.width - (textView.textContainerInset.left + textView.textContainerInset.right + 2 * textView.textContainer.lineFragmentPadding);
Which is a nice value to know about. You can use it in boundingRectWithSize: calls, for example:
CGRect boundingRect = [text boundingRectWithSize:CGSizeMake(wrappingWidth, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin // this is important
attributes:#{ NSFontAttributeName: textView.font } // and any other attributes of your text
context:nil];
Also cool is the fact that you can set textContainerInset and lineFragmentPadding, so if you want to increase this or have a UITextView that renders with no insets (so it matches a UILabel, for example), you can.
Nothing wrong with the answer above but I have been researching the UITextView word wrapping behavior and have discovered that although one can adjust the contentSize of the textview and the corresponding size of the textContainer, one additional method of changing the text wrap position (when using Auto Layout) is to make the trailing space to the SuperView into a negative value. This will allow one to push the word wrapping feature outward to the right. Not finished with the research just yet, but this tip should help anyone seeking to better control the UITextView word wrapping behavior.
Will post code once I'm complete but for now adjusting the trailing constraint of a UITextView will move the rightmost position of the word/character wrap.

iOS AutoLayout multi-line UILabel

Following question is sort-of continuation of this one:
iOS: Multi-line UILabel in Auto Layout
The main idea is that every view is supposed to state it's "preferred" (intrinsic) size so that AutoLayout can know how to display it properly.
UILabel is just an example of a situation where a view cannot by itself know what size it needs for display. It depends on what width is provided.
As mwhuss pointed out, setPreferredMaxLayoutWidth did the trick of making the label span across multiple lines. But that is not the main question here. The question is where and when do I get this width value that I send as an argument to setPreferredMaxLayoutWidth.
I managed to make something that looks legit, so correct me if I am wrong in any way and tell me please if you know a better way.
In the UIView's
-(CGSize) intrinsicContentSize
I setPreferredMaxLayoutWidth for my UILabels according to self.frame.width.
UIViewController's
-(void) viewDidLayoutSubviews
is the first callback method I know where subviews of the main view are appointed with their exact frames that they inhabit on the screen. From inside that method I, then, operate on my subviews, invalidating their intrinsic sizes so that UILabels are broken into multiple lines based on the width that was appointed to them.
There's an answer this question on objc.io in the "Intrinsic Content Size of Multi-Line Text" section of Advanced Auto Layout Toolbox. Here's the relevant info:
The intrinsic content size of UILabel and NSTextField is ambiguous for multi-line text. The height of the text depends on the width of the lines, which is yet to be determined when solving the constraints. In order to solve this problem, both classes have a new property called preferredMaxLayoutWidth, which specifies the maximum line width for calculating the intrinsic content size.
Since we usually don’t know this value in advance, we need to take a two-step approach to get this right. First we let Auto Layout do its work, and then we use the resulting frame in the layout pass to update the preferred maximum width and trigger layout again.
The code they give for use inside a view controller:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
[self.view layoutIfNeeded];
}
Take a look at their post, there's more information about why it's necessary to do the layout twice.
It seems annoying that a UILabel doesn't default to its width for the preferred max layout width, if you've got constraints that are unambiguously defining that width for you.
In nearly every single case I've used labels under Autolayout, the preferred max layout width has been the actual width of the label, once the rest of my layout has been performed.
So, to make this happen automatically, I have used a UILabel subclass, which overrides setBounds:. Here, call the super implementation, then, if it isn't the case already, set the preferred max layout width to be the bounds size width.
The emphasis is important - setting preferred max layout causes another layout pass to be performed, so you can end up with an infinite loop.
Update
My original answer appears to be helpful so I have left it untouched below, however, in my own projects I have found a more reliable solution that works around bugs in iOS 7 and iOS 8.
https://github.com/nicksnyder/ios-cell-layout
Original answer
This is a complete solution that works for me on iOS 7 and iOS 8
Objective C
#implementation AutoLabel
- (void)setBounds:(CGRect)bounds {
if (bounds.size.width != self.bounds.size.width) {
[self setNeedsUpdateConstraints];
}
[super setBounds:bounds];
}
- (void)updateConstraints {
if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
[super updateConstraints];
}
#end
Swift
import Foundation
class EPKAutoLabel: UILabel {
override var bounds: CGRect {
didSet {
if (bounds.size.width != oldValue.size.width) {
self.setNeedsUpdateConstraints();
}
}
}
override func updateConstraints() {
if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
self.preferredMaxLayoutWidth = self.bounds.size.width
}
super.updateConstraints()
}
}
We had a situation where an auto-layouted UILabel inside a UIScrollView laid out fine in portrait, but when rotated to landscape the height of the UILabel wasn't recalculated.
We found that the answer from #jrturton fixed this, presumably because now the preferredMaxLayoutWidth is correctly set.
Here's the code we used. Just set the Custom class from Interface builder to be CVFixedWidthMultiLineLabel.
CVFixedWidthMultiLineLabel.h
#interface CVFixedWidthMultiLineLabel : UILabel
#end
CVFixedWidthMultiLineLabel.m
#implementation CVFixedWidthMultiLineLabel
// Fix for layout failure for multi-line text from
// http://stackoverflow.com/questions/17491376/ios-autolayout-multi-line-uilabel
- (void) setBounds:(CGRect)bounds {
[super setBounds:bounds];
if (bounds.size.width != self.preferredMaxLayoutWidth) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
}
}
#end
Using boundingRectWithSize
I resolved my struggle with two multi-line labels in a legacy UITableViewCell that was using "\n" as a line-break by measuring the desired width like this:
- (CGFloat)preferredMaxLayoutWidthForLabel:(UILabel *)label
{
CGFloat preferredMaxLayoutWidth = 0.0f;
NSString *text = label.text;
UIFont *font = label.font;
if (font != nil) {
NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
NSDictionary *attributes = #{NSFontAttributeName: font,
NSParagraphStyleAttributeName: [mutableParagraphStyle copy]};
CGRect boundingRect = [text boundingRectWithSize:CGSizeZero options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
preferredMaxLayoutWidth = ceilf(boundingRect.size.width);
NSLog(#"Preferred max layout width for %# is %0.0f", text, preferredMaxLayoutWidth);
}
return preferredMaxLayoutWidth;
}
Then calling the method was then as simple as:
CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];
As I'm not allowed to add a comment, I'm obliged to add it as an answer.
The version of jrturton only worked for me if I call layoutIfNeeded in updateViewConstraints before getting the preferredMaxLayoutWidth of the label in question.
Without the call to layoutIfNeeded the preferredMaxLayoutWidth was always 0 in updateViewConstraints. And yet, it had always the desired value when checked in setBounds:. I didn't manage to get to know WHEN the correct preferredMaxLayoutWidth was set. I override setPreferredMaxLayoutWidth: on the UILabel subclass, but it never got called.
Summarized, I:
...sublcassed UILabel
...and override setBounds: to, if not already set, set preferredMaxLayoutWidth to CGRectGetWidth(bounds)
...call [super updateViewConstraints] before the following
...call layoutIfNeeded before getting preferredMaxLayoutWidth to be used in label's size calculation
EDIT: This workaround only seems to work, or be needed, sometimes. I just had an issue (iOS 7/8) where the label's height were not correctly calculated, as preferredMaxLayoutWidth returned 0 after the layout process had been executed once. So after some trial and error (and having found this Blog entry) I switched to using UILabel again and just set top, bottom, left and right auto layout constraints. And for whatever reason the label's height was set correctly after updating the text.
As suggested by another answer I tried to override viewDidLayoutSubviews:
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
_subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
[self.view layoutIfNeeded];
}
This worked, but it was visible on the UI and caused a "visible flicker" i.e. first the label was rendered with the height of two lines, then it was re-rendered with the height of only one line.
This was not acceptable for me.
I found then a better solution by overriding updateViewConstraints:
-(void)updateViewConstraints {
[super updateViewConstraints];
// Multiline-Labels and Autolayout do not work well together:
// In landscape mode the width is still "portrait" when the label determines the count of lines
// Therefore the preferredMaxLayoutWidth must be set
_subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
}
This was the better solution for me, because it did not cause the "visual flickering".
A clean solution is to set rowcount = 0 and to use a property for the heightconstraint of your label. Then after the content is set call
CGSize sizeThatFitsLabel = [_subtitleLabel sizeThatFits:CGSizeMake(_subtitleLabel.frame.size.width, MAXFLOAT)];
_subtitleLabelHeightConstraint.constant = ceilf(sizeThatFitsLabel.height);
-(void) updateViewConstraints has a problem since iOS 7.1.
In iOS 8, you can fix multi-line label layout problems in a cell by calling cell.layoutIfNeeded() after dequeuing and configuring the cell. The call is harmless in iOS 9.
See Nick Snyder's answer. This solution was taken from his code at https://github.com/nicksnyder/ios-cell-layout/blob/master/CellLayout/TableViewController.swift.

ios autolayout similar

I'm pretty new to ios. And while writing my first app i'v encountered that autolayout is only for ios 6.0. And i'd wish to make my app for at least 5.0 ios.
Maybe anyone would know how to make this without autolayout.
I have label which has dynamic text, 1row or 2 rows or 3 rows depends on user settings. And below it i have uitextfield. With autolayout i have no headache as it does all the work, the textfield sits nicely below in 1 2 or 3 rows of text above (resizes, moves automatically).
So how should i do this without autolayout?
Without autolayout you have to handle this in code. The recommended way of doing this would be to subclass your container view (the view that contains your label and text filed) and override the layoutSubviews method. In there you set the frames of the view's subviews manually, based on your desired criteria (e.g., the label text metrics).
EDIT: here's a specific example of something that could be in the containverView layoutSubviews method (typed from the top of my head):
// Those could be IBOutlets, or obtained by inspecting self.subviews
UILabel *label = self.label;
UITextField *textField = self.textField;
// Determine the labelSize, we could limit the maxSize (especially the height) manually here
CGSize maxSize = self.bounds.size;
CGSize labelSize = [label.text sizeWithFont:label.font constrainedToSize:maxSize lineBreakMode:label.lineBreakMode];
// Set the computed label size
CGRect labelFrame = label.frame;
labelFrame.size = labelSize;
label.frame = labelFrame;
// Now move the textField just below the label (we could also add a vertical margin here if we want)
CGRect textFieldFrame = textField.frame;
textFieldFrame.origin.y = labelFrame.origin.y + labelFrame.size.height;
textField.frame = textFieldFrame;
This just makes sure the text field is always below the label. Depending on the constraints you had in place, you might need to add more code to make sure the UI lays out correctly.
You also need to make sure that [containverView setNeedsLayout] gets called when the label text changes.
if you are using nib or storybord you can just remove the mark in the file inspector

Resources