'Add "...Read More" to the end of UILabel
I have a UILabel
and in some cases the text is longer then the UILabel
itself, so I see the text as "bla bla bla..."
I want to add a ...Read More
button text at the end of the UILabel
..
I've read some posts but they offer solutions that are not good to me, for example: to calculate how many characters will enter the UILabel
, but with the font i'm using each character has a different width.
How can I manage to do that?
Thanks in advance!
Solution 1:[1]
So this is what I did to add the Read More... button to the UITextView
, UITextField
or UILabel
:
- (void)addReadMoreStringToUILabel:(UILabel*)label
{
NSString *readMoreText = @" ...Read More";
NSInteger lengthForString = label.text.length;
if (lengthForString >= 30)
{
NSInteger lengthForVisibleString = [self fitString:label.text intoLabel:label];
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:label.text];
NSString *trimmedString = [mutableString stringByReplacingCharactersInRange:NSMakeRange(lengthForVisibleString, (label.text.length - lengthForVisibleString)) withString:@""];
NSInteger readMoreLength = readMoreText.length;
NSString *trimmedForReadMore = [trimmedString stringByReplacingCharactersInRange:NSMakeRange((trimmedString.length - readMoreLength), readMoreLength) withString:@""];
NSMutableAttributedString *answerAttributed = [[NSMutableAttributedString alloc] initWithString:trimmedForReadMore attributes:@{
NSFontAttributeName : label.font
}];
NSMutableAttributedString *readMoreAttributed = [[NSMutableAttributedString alloc] initWithString:readMoreText attributes:@{
NSFontAttributeName : Font(TWRegular, 12.),
NSForegroundColorAttributeName : White
}];
[answerAttributed appendAttributedString:readMoreAttributed];
label.attributedText = answerAttributed;
UITagTapGestureRecognizer *readMoreGesture = [[UITagTapGestureRecognizer alloc] initWithTarget:self action:@selector(readMoreDidClickedGesture:)];
readMoreGesture.tag = 1;
readMoreGesture.numberOfTapsRequired = 1;
[label addGestureRecognizer:readMoreGesture];
label.userInteractionEnabled = YES;
}
else {
NSLog(@"No need for 'Read More'...");
}
}
There is a use of fitString:intoLabel
method which can be found here.
As for the UITagTapGestureRecognizer
is just a normal UITapGestureRecognizer
subclass with a NSInteger
property called tag. I did that because I want to identify which Read More...
were clicked in I case I have more than one in the same UIViewController
. You can use a normal UITapGestureRecognizer
.
Enjoy!
Solution 2:[2]
Swift4 (IOS 11.2)
Readmore at the end of the label without action
extension UILabel {
func addTrailing(with trailingText: String, moreText: String, moreTextFont: UIFont, moreTextColor: UIColor) {
let readMoreText: String = trailingText + moreText
let lengthForVisibleString: Int = self.visibleTextLength
let mutableString: String = self.text!
let trimmedString: String? = (mutableString as NSString).replacingCharacters(in: NSRange(location: lengthForVisibleString, length: ((self.text?.count)! - lengthForVisibleString)), with: "")
let readMoreLength: Int = (readMoreText.count)
let trimmedForReadMore: String = (trimmedString! as NSString).replacingCharacters(in: NSRange(location: ((trimmedString?.count ?? 0) - readMoreLength), length: readMoreLength), with: "") + trailingText
let answerAttributed = NSMutableAttributedString(string: trimmedForReadMore, attributes: [NSAttributedStringKey.font: self.font])
let readMoreAttributed = NSMutableAttributedString(string: moreText, attributes: [NSAttributedStringKey.font: moreTextFont, NSAttributedStringKey.foregroundColor: moreTextColor])
answerAttributed.append(readMoreAttributed)
self.attributedText = answerAttributed
}
var visibleTextLength: Int {
let font: UIFont = self.font
let mode: NSLineBreakMode = self.lineBreakMode
let labelWidth: CGFloat = self.frame.size.width
let labelHeight: CGFloat = self.frame.size.height
let sizeConstraint = CGSize(width: labelWidth, height: CGFloat.greatestFiniteMagnitude)
let attributes: [AnyHashable: Any] = [NSAttributedStringKey.font: font]
let attributedText = NSAttributedString(string: self.text!, attributes: attributes as? [NSAttributedStringKey : Any])
let boundingRect: CGRect = attributedText.boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, context: nil)
if boundingRect.size.height > labelHeight {
var index: Int = 0
var prev: Int = 0
let characterSet = CharacterSet.whitespacesAndNewlines
repeat {
prev = index
if mode == NSLineBreakMode.byCharWrapping {
index += 1
} else {
index = (self.text! as NSString).rangeOfCharacter(from: characterSet, options: [], range: NSRange(location: index + 1, length: self.text!.count - index - 1)).location
}
} while index != NSNotFound && index < self.text!.count && (self.text! as NSString).substring(to: index).boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, attributes: attributes as? [NSAttributedStringKey : Any], context: nil).size.height <= labelHeight
return prev
}
return self.text!.count
}
}
Swift 4.2
extension UILabel {
func addTrailing(with trailingText: String, moreText: String, moreTextFont: UIFont, moreTextColor: UIColor) {
let readMoreText: String = trailingText + moreText
let lengthForVisibleString: Int = self.vissibleTextLength
let mutableString: String = self.text!
let trimmedString: String? = (mutableString as NSString).replacingCharacters(in: NSRange(location: lengthForVisibleString, length: ((self.text?.count)! - lengthForVisibleString)), with: "")
let readMoreLength: Int = (readMoreText.count)
let trimmedForReadMore: String = (trimmedString! as NSString).replacingCharacters(in: NSRange(location: ((trimmedString?.count ?? 0) - readMoreLength), length: readMoreLength), with: "") + trailingText
let answerAttributed = NSMutableAttributedString(string: trimmedForReadMore, attributes: [NSAttributedString.Key.font: self.font])
let readMoreAttributed = NSMutableAttributedString(string: moreText, attributes: [NSAttributedString.Key.font: moreTextFont, NSAttributedString.Key.foregroundColor: moreTextColor])
answerAttributed.append(readMoreAttributed)
self.attributedText = answerAttributed
}
var vissibleTextLength: Int {
let font: UIFont = self.font
let mode: NSLineBreakMode = self.lineBreakMode
let labelWidth: CGFloat = self.frame.size.width
let labelHeight: CGFloat = self.frame.size.height
let sizeConstraint = CGSize(width: labelWidth, height: CGFloat.greatestFiniteMagnitude)
let attributes: [AnyHashable: Any] = [NSAttributedString.Key.font: font]
let attributedText = NSAttributedString(string: self.text!, attributes: attributes as? [NSAttributedString.Key : Any])
let boundingRect: CGRect = attributedText.boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, context: nil)
if boundingRect.size.height > labelHeight {
var index: Int = 0
var prev: Int = 0
let characterSet = CharacterSet.whitespacesAndNewlines
repeat {
prev = index
if mode == NSLineBreakMode.byCharWrapping {
index += 1
} else {
index = (self.text! as NSString).rangeOfCharacter(from: characterSet, options: [], range: NSRange(location: index + 1, length: self.text!.count - index - 1)).location
}
} while index != NSNotFound && index < self.text!.count && (self.text! as NSString).substring(to: index).boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, attributes: attributes as? [NSAttributedString.Key : Any], context: nil).size.height <= labelHeight
return prev
}
return self.text!.count
}
}
Usage
let readmoreFont = UIFont(name: "Helvetica-Oblique", size: 11.0)
let readmoreFontColor = UIColor.blue
DispatchQueue.main.async {
self.yourLabel.addTrailing(with: "... ", moreText: "Readmore", moreTextFont: readmoreFont!, moreTextColor: readmoreFontColor)
}
Result
NOTE: - Action is not included for Readmore
Solution 3:[3]
Tttattributed label has this feature
https://github.com/TTTAttributedLabel/TTTAttributedLabel
You need to set the "truncation" token as "read more..."
See
attributedTruncationToken
var subTitleLabel = TTTAttributedLabel(frame : frame)
self.addSubview(subTitleLabel)
var trunc = NSMutableAttributedString(string: "...more")
trunc.addAttribute(NSFontAttributeName, value: UIFont.systemFontOfSize(12), range: NSMakeRange(0, 7))
trunc.addAttribute(NSForegroundColorAttributeName, value: UIColor.blueColor(), range: NSMakeRange(0, 7))
subTitleLabel.attributedTruncationToken = trunc
subTitleLabel.numberOfLines = 1
subTitleLabel.autoresizingMask = UIViewAutoresizing.FlexibleHeight | UIViewAutoresizing.FlexibleWidth
Solution 4:[4]
This works for Swift 5
Here is a safer version of @ramchandran's answer because you don’t know how many characters the user will enter.
In his answer if the length of the string the user entered is less then the length of the whatever text you decide to use for ... Readmore
then it will crash. For eg this is how you use it
if yourLabel.text!.count > 1 {
let readmoreFont = UIFont(name: "Helvetica-Oblique", size: 11.0)
let readmoreFontColor = UIColor.blue
DispatchQueue.main.async {
self.yourLabel.addTrailing(with: "... ", moreText: "Readmore", moreTextFont: readmoreFont!, moreTextColor: readmoreFontColor)
}
}
In the above example the output of ... Readmore
is 12 characters total. If the string the user entered was yourLabel.text = "12345678"
then the string's text would only be 8 characters.
It would crash because the range using ((trimmedString?.count ?? 0) - readMoreLength)
in the line below would produce a negative result:
// “12345678” minus “... Readmore” = negative four (8 - 12 = -4)
let trimmedForReadMore: String = (trimmedString! as NSString).replacingCharacters(in: NSRange(location: ((trimmedString?.count ?? 0) - readMoreLength), length: readMoreLength), with: "") + trailingText
I added a safety check to make sure that if the string entered is less then or equal to the number of characters for whatever you decide to use as ... Readmore
it will return and the line that will cause the crash will never get reached:
// trimmedString is the string the user entered
guard let safeTrimmedString = trimmedString else { return }
if safeTrimmedString.count <= readMoreLength { return }
It's located in the center of the addTrailing
function
extension UILabel{
func addTrailing(with trailingText: String, moreText: String, moreTextFont: UIFont, moreTextColor: UIColor) {
let readMoreText: String = trailingText + moreText
if self.visibleTextLength == 0 { return }
let lengthForVisibleString: Int = self.visibleTextLength
if let myText = self.text {
let mutableString: String = myText
let trimmedString: String? = (mutableString as NSString).replacingCharacters(in: NSRange(location: lengthForVisibleString, length: myText.count - lengthForVisibleString), with: "")
let readMoreLength: Int = (readMoreText.count)
guard let safeTrimmedString = trimmedString else { return }
if safeTrimmedString.count <= readMoreLength { return }
print("this number \(safeTrimmedString.count) should never be less\n")
print("then this number \(readMoreLength)")
// "safeTrimmedString.count - readMoreLength" should never be less then the readMoreLength because it'll be a negative value and will crash
let trimmedForReadMore: String = (safeTrimmedString as NSString).replacingCharacters(in: NSRange(location: safeTrimmedString.count - readMoreLength, length: readMoreLength), with: "") + trailingText
let answerAttributed = NSMutableAttributedString(string: trimmedForReadMore, attributes: [NSAttributedString.Key.font: self.font])
let readMoreAttributed = NSMutableAttributedString(string: moreText, attributes: [NSAttributedString.Key.font: moreTextFont, NSAttributedString.Key.foregroundColor: moreTextColor])
answerAttributed.append(readMoreAttributed)
self.attributedText = answerAttributed
}
}
var visibleTextLength: Int {
let font: UIFont = self.font
let mode: NSLineBreakMode = self.lineBreakMode
let labelWidth: CGFloat = self.frame.size.width
let labelHeight: CGFloat = self.frame.size.height
let sizeConstraint = CGSize(width: labelWidth, height: CGFloat.greatestFiniteMagnitude)
if let myText = self.text {
let attributes: [AnyHashable: Any] = [NSAttributedString.Key.font: font]
let attributedText = NSAttributedString(string: myText, attributes: attributes as? [NSAttributedString.Key : Any])
let boundingRect: CGRect = attributedText.boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, context: nil)
if boundingRect.size.height > labelHeight {
var index: Int = 0
var prev: Int = 0
let characterSet = CharacterSet.whitespacesAndNewlines
repeat {
prev = index
if mode == NSLineBreakMode.byCharWrapping {
index += 1
} else {
index = (myText as NSString).rangeOfCharacter(from: characterSet, options: [], range: NSRange(location: index + 1, length: myText.count - index - 1)).location
}
} while index != NSNotFound && index < myText.count && (myText as NSString).substring(to: index).boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, attributes: attributes as? [NSAttributedString.Key : Any], context: nil).size.height <= labelHeight
return prev
}
}
if self.text == nil {
return 0
} else {
return self.text!.count
}
}
}
Solution 5:[5]
Swift 4 and Swift 5. I need to implement the same. As answers are already given but according to me TTTAttributedLabel is the best way to do it. It gives you better control over content. Easy to find address, link, date etc. You can also change the color of links. TTTAttributedLabel Library link is already given in above answer. Lets come to implementation.
let kCharacterBeforReadMore = 20
let kReadMoreText = "...ReadMore"
let kReadLessText = "...ReadLess"
@IBOutlet weak var labelText: TTTAttributedLabel! // setYouLabel Class to TTTAttributedLabel in StoryBoard
var strFull = ""
override func viewDidLoad() {
super.viewDidLoad()
strFull = "I need to implement the same. As answers are already given but according to me TTTAttributedLabel is the best way to do it. I gives I need to implement the same. As answers are already given but according to me TTTAttributedLabel is the best way to do it. I gives you"
labelText.showTextOnTTTAttributeLable(str: strFull, readMoreText: kReadMoreText, readLessText: kReadLessText, font: UIFont.init(name: "Helvetica-Bold", size: 24.0)!, charatersBeforeReadMore: kCharacterBeforReadMore, activeLinkColor: UIColor.blue, isReadMoreTapped: false, isReadLessTapped: false)
labelText.delegate = self
}
func readMore(readMore: Bool) {
labelText.showTextOnTTTAttributeLable(str: strFull, readMoreText: kReadMoreText, readLessText: kReadLessText, font: nil, charatersBeforeReadMore: kCharacterBeforReadMore, activeLinkColor: UIColor.blue, isReadMoreTapped: readMore, isReadLessTapped: false)
}
func readLess(readLess: Bool) {
labelText.showTextOnTTTAttributeLable(str: strFull, readMoreText: kReadMoreText, readLessText: kReadLessText, font: nil, charatersBeforeReadMore: kCharacterBeforReadMore, activeLinkColor: UIColor.blue, isReadMoreTapped: readLess, isReadLessTapped: true)
}
}
Here I have crated an extension of TTTAttributedLabel and put the ReadMore and ReadLess logic here. You can modify according to your.
extension TTTAttributedLabel {
func showTextOnTTTAttributeLable(str: String, readMoreText: String, readLessText: String, font: UIFont?, charatersBeforeReadMore: Int, activeLinkColor: UIColor, isReadMoreTapped: Bool, isReadLessTapped: Bool) {
let text = str + readLessText
let attributedFullText = NSMutableAttributedString.init(string: text)
let rangeLess = NSString(string: text).range(of: readLessText, options: String.CompareOptions.caseInsensitive)
//Swift 5
// attributedFullText.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.blue], range: rangeLess)
attributedFullText.addAttributes([NSAttributedString.Key.foregroundColor : UIColor.blue], range: rangeLess)
var subStringWithReadMore = ""
if text.count > charatersBeforeReadMore {
let start = String.Index(encodedOffset: 0)
let end = String.Index(encodedOffset: charatersBeforeReadMore)
subStringWithReadMore = String(text[start..<end]) + readMoreText
}
let attributedLessText = NSMutableAttributedString.init(string: subStringWithReadMore)
let nsRange = NSString(string: subStringWithReadMore).range(of: readMoreText, options: String.CompareOptions.caseInsensitive)
//Swift 5
// attributedLessText.addAttributes([NSAttributedStringKey.foregroundColor : UIColor.blue], range: nsRange)
attributedLessText.addAttributes([NSAttributedString.Key.foregroundColor : UIColor.blue], range: nsRange)
// if let _ = font {// set font to attributes
// self.font = font
// }
self.attributedText = attributedLessText
self.activeLinkAttributes = [NSAttributedString.Key.foregroundColor : UIColor.blue]
//Swift 5
// self.linkAttributes = [NSAttributedStringKey.foregroundColor : UIColor.blue]
self.linkAttributes = [NSAttributedString.Key.foregroundColor : UIColor.blue]
self.addLink(toTransitInformation: ["ReadMore":"1"], with: nsRange)
if isReadMoreTapped {
self.numberOfLines = 0
self.attributedText = attributedFullText
self.addLink(toTransitInformation: ["ReadLess": "1"], with: rangeLess)
}
if isReadLessTapped {
self.numberOfLines = 3
self.attributedText = attributedLessText
}
}
}
You need to implement the didSelectLinkWithTransitInformation delegate of TTTAttributedLabel. Here you can get the component which you have passed
extension ViewController: TTTAttributedLabelDelegate {
func attributedLabel(_ label: TTTAttributedLabel!, didSelectLinkWithTransitInformation components: [AnyHashable : Any]!) {
if let _ = components as? [String: String] {
if let value = components["ReadMore"] as? String, value == "1" {
self.readMore(readMore: true)
}
if let value = components["ReadLess"] as? String, value == "1" {
self.readLess(readLess: true)
}
}
}
}
Result- Before tapping ReadMore
Result- After tapping ReadMore
Solution 6:[6]
class DynamicLabel: UILabel{
var fullText: String?
var truncatedLength = 100
var isTruncated = true
func collapse(){
let index = fullText!.index(fullText!.startIndex, offsetBy: truncatedLength)
self.text = fullText![...index].description + "... More"
isTruncated = true
}
func expand(){
self.text = fullText
isTruncated = false
}
}
Just a simple trick to get through all these messy implementations. The idea is simple, we don't set collapsed or expand rows, just set the label to 0.
Then store the original text on fullText
variable. Now if we want to display collapsed format, then just get substring and add the custom ellipsis.
Note: This does not include tap event handlers, you can add it yourself on the controller.
Solution 7:[7]
Using method - boundingRectWithSize:options:attributes:context: and passing your font as NSFontAttributeName
key for NSAttributedString
will give you the correct rect needed.
From that you need to check if it's bigger than your label bounds minus offset. Only if yes, you need to trim your text and show Read More
at the end.
Solution 8:[8]
My solution is, I create a UIButton
(name Read more) at bottom-right and below the UILabel
.
After that I check the UILabel
is truncated or not for showing or hiding the UIButton
CGSize sizeOfText = [self.label.text boundingRectWithSize: CGSizeMake(self.label.intrinsicContentSize.width, CGFLOAT_MAX)
options: (NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
attributes: [NSDictionary dictionaryWithObject:self.label.font forKey:NSFontAttributeName] context: nil].size;
if (self.label.intrinsicContentSize.height < ceilf(sizeOfText.height)) {
// label is truncated
self.readmoreButton.hidden = NO; // show Read more button
}else{
self.readmoreButton.hidden = YES;
}
=== Swift 3 version
let textheight = self.label.text?.height(withConstrainedWidth: self.label.frame.width, font: self.label.font)
if self.label.intrinsicContentSize.height < textheight! {
self.readmoreButton.isHidden = false
}else{
self.readmoreButton.isHidden = true
}
add this extension:
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName: font], context: nil)
return boundingBox.height
}
}
Hope this help
Solution 9:[9]
You can try the 3rd library ExpandableLabel
Set the custom class of your UILabel to ExpandableLabel and set the desired number of lines and collapsed text:
expandableLabel.numberOfLines = 5
expandableLabel.collapsedAttributedLink = NSAttributedString(string: "more")
expandableLabel.ellipsis = NSAttributedString(string: "...")
// update label expand or collapse state
expandableLabel.collapsed = true
You may need to set a delegate
to get notified in case the link has been touched.
Solution 10:[10]
this method is useful for showless and showAll with updown arrow image: add tapgesture on label
viewcontroller.h
@property (nonatomic,assign) BOOL isReadable;
viewcontrollr.m
#pragma mark :- Tap Gesture View All
-(void)readMoreDidClickedGesture :(UITapGestureRecognizer
*)objTapGesture{
UILabel * lblDesc = (UILabel *)[objTapGesture view];
NSLog(@"%@",lblDesc.text);
if (self.isReadable == false) {
[self setIsReadable:YES];
lblDesc.text = readmoreText;
readMoreHeight = [self getLabelHeight:lblDesc];
}
else{
readMoreHeight = 30.0;
[self setIsReadable:NO];
}
}
- (void)addReadMoreStringToUILabel:(UILabel*)label isReaded:(BOOL)isReaded
{
NSString *readMoreText = (isReaded == false) ? @"...Show All " :
@"Show Less ";
NSInteger lengthForString = label.text.length;
if (lengthForString >= 30)
{
NSInteger lengthForVisibleString = [self getLabelHeight:label];//[self fitString:label.text intoLabel:label];
NSMutableString *mutableString = [[NSMutableString alloc] initWithString:label.text];
readmoreText = mutableString;
NSString *trimmedString = [mutableString stringByReplacingCharactersInRange:NSMakeRange(lengthForVisibleString, (label.text.length - lengthForVisibleString)) withString:@""];
NSInteger readMoreLength = readMoreText.length;
NSString *trimmedForReadMore = [trimmedString stringByReplacingCharactersInRange:NSMakeRange((trimmedString.length - readMoreLength), readMoreLength) withString:@""];
NSMutableAttributedString *answerAttributed = [[NSMutableAttributedString alloc] initWithString:trimmedForReadMore attributes:@{
NSFontAttributeName : label.font
}];
NSMutableAttributedString *readMoreAttributed = [[NSMutableAttributedString alloc] initWithString:readMoreText attributes:@{
NSFontAttributeName :label.font, NSForegroundColorAttributeName :[UIColor orangeColor]
}];
if (isReaded == false){
[readMoreAttributed addAttribute:NSUnderlineStyleAttributeName
value:@(NSUnderlineStyleSingle)
range:NSMakeRange(3, 8)];
NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
UIImageView *imgDown = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 25, 25)];
imgDown.image = [UIImage imageNamed:@"searchFilterArrow1"];
imgDown.image = [imgDown.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[imgDown setTintColor:[UIColor orangeColor]];
textAttachment.image = imgDown.image;
NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment];
[readMoreAttributed replaceCharactersInRange:NSMakeRange(12, 1) withAttributedString:attrStringWithImage];
}
else{
[readMoreAttributed addAttribute:NSUnderlineStyleAttributeName
value:@(NSUnderlineStyleSingle)
range:NSMakeRange(1, 9)];
NSTextAttachment *textAttachment = [[NSTextAttachment alloc] init];
UIImageView *imgup = [[UIImageView alloc]initWithFrame:CGRectMake(0, 0, 25, 25)];
imgup.image = [UIImage imageNamed:@"searchFilterArrow2"];
imgup.image = [imgup.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[imgup setTintColor:[UIColor orangeColor]];
textAttachment.image = imgup.image;
NSAttributedString *attrStringWithImage = [NSAttributedString attributedStringWithAttachment:textAttachment];
[readMoreAttributed replaceCharactersInRange:NSMakeRange(11, 1) withAttributedString:attrStringWithImage];
}
[answerAttributed appendAttributedString:readMoreAttributed];
label.attributedText = answerAttributed;
UITapGestureRecognizer *readMoreGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(readMoreDidClickedGesture:)];
readMoreGesture.numberOfTapsRequired = 1;
[label addGestureRecognizer:readMoreGesture];
label.userInteractionEnabled = YES;
}
else {
NSLog(@"No need for 'Read More'...");
}
}
Solution 11:[11]
func updateData(_ label: UILabel) {
self.headerLabel.text = detailViewModel.firstTitle
self.detailLabel.text = detailViewModel.firstContent
headerTitle = detailViewModel.firstTitle
detailTitle = detailViewModel.firstContent
DispatchQueue.main.async {
let readMoreText = "...View More"
let stringColor: UIColor = UIColor.blue
let attributes = [NSForegroundColorAttributeName: stringColor]
let numberOfLines = self.detailLabel.numberOfVisibleLines
if numberOfLines > 2 {
let lengthForVisibleString: Int = self.fit( self.detailLabel.text, into: self.detailLabel)
let mutableString = self.detailLabel.text ?? ""
let trimmedString = (mutableString as NSString).replacingCharacters(in: NSRange(location: lengthForVisibleString, length: (self.detailLabel?.text?.count ?? 0) - lengthForVisibleString), with: "")
let readMoreLength: Int = readMoreText.count
let trimmedForReadMore = (trimmedString as NSString).replacingCharacters(in: NSRange(location: trimmedString.count - readMoreLength, length: readMoreLength), with: "")
let answerAttributed = NSMutableAttributedString(string: trimmedForReadMore, attributes: [NSFontAttributeName: self.detailLabel.font])
let readMoreAttributed = NSMutableAttributedString(string: readMoreText, attributes: attributes)
answerAttributed.append(readMoreAttributed)
self.detailLabel.attributedText = answerAttributed
let readMoreGesture = UITapGestureRecognizer(target: self, action:#selector(FundDetailsTableViewCell.showViewMore(_:)))
readMoreGesture.numberOfTapsRequired = 1
self.detailLabel.addGestureRecognizer(readMoreGesture)
self.detailLabel.isUserInteractionEnabled = true
}
}
}
func fit(_ string: String?, into label: UILabel?) -> Int {
guard let stringObjc = string as NSString? else {
return 0
}
let font: UIFont = label?.font ?? UIFont.systemFont(ofSize: 14.0)
let mode: NSLineBreakMode? = label?.lineBreakMode
let labelWidth: CGFloat? = label?.frame.size.width
let labelHeight: CGFloat? = label?.frame.size.height
let sizeConstraint = CGSize(width: labelWidth ?? 0.0, height: CGFloat.greatestFiniteMagnitude)
let attributes = [NSFontAttributeName: font]
let device = UIDevice.current
let iosVersion = Double(device.systemVersion) ?? 0
if iosVersion > 7 {
let attributedText = NSAttributedString(string: string ?? "", attributes: attributes)
let boundingRect: CGRect = attributedText.boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, context: nil)
do {
if boundingRect.size.height > (labelHeight ?? 0.0) {
var index: Int = 0
var prev: Int
let characterSet = CharacterSet.whitespacesAndNewlines
repeat {
prev = index
if mode == .byCharWrapping {
index += 1
} else {
index = Int((string as NSString?)?.rangeOfCharacter(from: characterSet, options: [], range: NSRange(location: index + 1, length: (string?.count ?? 0) - index - 1)).location ?? 0)
}
} while index != NSNotFound && index < (string?.count ?? 0)
&& (stringObjc.substring(to: index).boundingRect(with: sizeConstraint, options: .usesLineFragmentOrigin, attributes: attributes, context: nil).size.height) <= labelHeight!
return prev;
}
}
} else {
if stringObjc.size(attributes: attributes).height > labelHeight! {
var index: Int = 0
var prev: Int
let characterSet = CharacterSet.whitespacesAndNewlines
repeat {
prev = index
if mode == .byCharWrapping {
index += 1
} else {
index = stringObjc.rangeOfCharacter(from: characterSet, options: NSString.CompareOptions.caseInsensitive, range: NSRange(location: index + 1, length: stringObjc.length - index - 1)).location
}
} while index != NSNotFound && index < (string?.count)! && (stringObjc.substring(to: index) as NSString).size(attributes: attributes).height <= labelHeight!
return prev
}
}
return (string?.count)!
}
func showViewMore(_ sender: UITapGestureRecognizer) {
}
extension UILabel {
var numberOfVisibleLines: Int {
let textSize = CGSize(width: CGFloat(self.frame.size.width), height: CGFloat(MAXFLOAT))
let rHeight: Int = lroundf(Float(self.sizeThatFits(textSize).height))
let charSize: Int = lroundf(Float(self.font.pointSize))
return rHeight / charSize
}
}
Solution 12:[12]
For Action
let tap = UITapGestureRecognizer(target: self, action: #selector(self.tapFunction))
Urlabel.isUserInteractionEnabled = true
Urlabel.addGestureRecognizer(tap)
@objc
func tapFunction(sender:UITapGestureRecognizer) {
}
Solution 13:[13]
For Action on the label, if using a CollectionView or TableView you could use a delegate method to perform the action.
func showMore(cell: CustomCell) {
guard let indexPath = self.tableView.indexPath(for: cell) else {
return
}
let cell = tableView.cellForRow(at: indexPath) as! CustomCell
tableView.beginUpdates()
cell.label.text = "your complete text"
tableView.endUpdates()
}
This updates the label and displays the full text as required Using Lance Samaria answer and adding the action for the cell.
Solution 14:[14]
Here is another solution with Swift 5.
Reference
- UITextView: Find location of ellipsis in truncated text
- https://stackoverflow.com/users/1847511/naloiko-eugene solution
Result
Steps
Logic is simple.
- check if the label's text is truncated or not.
- get an index of the starting point of ellipsis in the label's text using LayoutManager
- slice the text using the index of ellipsis
- replace the substrings with
... more
(You can change it) - add UITapGesture on the label
- get the index in the label's attributedString from gestrure location
->
private func getIndex(from point: CGPoint) -> Int?
- check the gesture location
->
func didTapInRange(_ point: CGPoint, targetRange: NSRange) -> Bool
Sample Code
let loremIpsumString = """
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
"""
private var expandableTextRange: NSRange?
//...in ViewDidLoad
label.text = loremIpsumString
if label.isTruncatedText {
expandableTextRange = label.setExpandActionIfPossible("More", textColor: .brown)
}
//Add IBAction on the label
@IBAction func didTapLabel(_ sender: UITapGestureRecognizer) {
guard let expandRange = expandableTextRange else {
return
}
let tapLocation = sender.location(in: label)
if label.didTapInRange(tapLocation, targetRange: expandRange) {
label.numberOfLines = 0
label.text = loremIpsumString
}
else {
resultLabel.text = "You tapped the area outside More."
}
}
extension UILabel {
var isTruncatedText: Bool {
guard let height = textHeight else {
return false
}
return height > bounds.size.height
}
var textHeight: CGFloat? {
guard let labelText = text else {
return nil
}
let attributes: [NSAttributedString.Key: UIFont] = [.font: font]
let labelTextSize = (labelText as NSString).boundingRect(
with: CGSize(width: frame.size.width, height: .greatestFiniteMagnitude),
options: .usesLineFragmentOrigin,
attributes: attributes,
context: nil
).size
return ceil(labelTextSize.height)
}
@discardableResult
func setExpandActionIfPossible(_ text: String, textColor: UIColor? = nil) -> NSRange? {
guard isTruncatedText, let visibleString = visibleText else {
return nil
}
let defaultTruncatedString = "... "
let fontAttribute: [NSAttributedString.Key: UIFont] = [.font: font]
let expandAttributedString: NSMutableAttributedString = NSMutableAttributedString(
string: defaultTruncatedString,
attributes: fontAttribute
)
let customExpandAttributes: [NSAttributedString.Key: Any] = [
.font: font as Any,
.foregroundColor: (textColor ?? self.textColor) as Any
]
let customExpandAttributedString = NSAttributedString(string: "\(text)", attributes: customExpandAttributes)
expandAttributedString.append(customExpandAttributedString)
let visibleAttributedString = NSMutableAttributedString(string: visibleString, attributes: fontAttribute)
guard visibleAttributedString.length > expandAttributedString.length else {
return nil
}
let changeRange = NSRange(location: visibleAttributedString.length - expandAttributedString.length, length: expandAttributedString.length)
visibleAttributedString.replaceCharacters(in: changeRange, with: expandAttributedString)
attributedText = visibleAttributedString
return changeRange
}
var visibleText: String? {
guard isTruncatedText,
let labelText = text,
let lastIndex = truncationIndex else {
return nil
}
let visibleTextRange = NSRange(location: 0, length: lastIndex)
guard let range = Range(visibleTextRange, in: labelText) else {
return nil
}
return String(labelText[range])
}
//https://stackoverflow.com/questions/41628215/uitextview-find-location-of-ellipsis-in-truncated-text/63797174#63797174
var truncationIndex: Int? {
guard let text = text, isTruncatedText else {
return nil
}
let attributes: [NSAttributedString.Key: UIFont] = [.font: font]
let attributedString = NSAttributedString(string: text, attributes: attributes)
let textContainer = NSTextContainer(
size: CGSize(width: frame.size.width,
height: CGFloat.greatestFiniteMagnitude)
)
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
let textStorage = NSTextStorage(attributedString: attributedString)
textStorage.addLayoutManager(layoutManager)
//Determine the range of all Glpyhs within the string
var glyphRange = NSRange()
layoutManager.glyphRange(
forCharacterRange: NSMakeRange(0, attributedString.length),
actualCharacterRange: &glyphRange
)
var truncationIndex = NSNotFound
//Iterate over each 'line fragment' (each line as it's presented, according to your `textContainer.lineBreakMode`)
var i = 0
layoutManager.enumerateLineFragments(
forGlyphRange: glyphRange
) { rect, usedRect, textContainer, glyphRange, stop in
if (i == self.numberOfLines - 1) {
//We're now looking at the last visible line (the one at which text will be truncated)
let lineFragmentTruncatedGlyphIndex = glyphRange.location
if lineFragmentTruncatedGlyphIndex != NSNotFound {
truncationIndex = layoutManager.truncatedGlyphRange(inLineFragmentForGlyphAt: lineFragmentTruncatedGlyphIndex).location
}
stop.pointee = true
}
i += 1
}
return truncationIndex
}
//https://stackoverflow.com/questions/1256887/create-tap-able-links-in-the-nsattributedstring-of-a-uilabel
private func getIndex(from point: CGPoint) -> Int? {
guard let attributedString = attributedText, attributedString.length > 0 else {
return nil
}
let textStorage = NSTextStorage(attributedString: attributedString)
let layoutManager = NSLayoutManager()
textStorage.addLayoutManager(layoutManager)
let textContainer = NSTextContainer(size: frame.size)
textContainer.lineFragmentPadding = 0
textContainer.maximumNumberOfLines = numberOfLines
textContainer.lineBreakMode = lineBreakMode
layoutManager.addTextContainer(textContainer)
let index = layoutManager.characterIndex(
for: point,
in: textContainer,
fractionOfDistanceBetweenInsertionPoints: nil
)
return index
}
func didTapInRange(_ point: CGPoint, targetRange: NSRange) -> Bool {
guard let indexOfPoint = getIndex(from: point) else {
return false
}
return indexOfPoint > targetRange.location &&
indexOfPoint < targetRange.location + targetRange.length
}
}
Solution 15:[15]
TTTAttributedLabel:- Use below line of code to set font
attributedLessText = NSMutableAttributedString(string: subStringWithReadMore, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17)])
Solution 16:[16]
I couldn't get anything here to work so I'm posting my solution. I had a UILabel that was supposed to be 3 lines and at the end of the text it should read ... Read more. I made a UIFont extension that calculates the height of a string with a particular font and width and then I made a String extension that recursively makes a string smaller until it fits into x lines.
extension String {
/**
This method returns a substring of a string that fits in a label with a specific width and number of lines
It has optional suffix so you can add your own ellipsis, like "... Read more"
- Parameter width: The label width that constrains the text. Make sure to call it after subViews have been laid out
- Parameter lines: Number of allowed lines in the label
- Parameter font: The font to use with the label
- Parameter suffix: Custom string that will be added to the string and will fit within the width/lines constraint
- Returns: A substring that fits within the constraints given
*/
func textThatFits(width: CGFloat, lines: Int, font: UIFont, suffix: String = "") -> String {
let lineHeight = font.lineHeight
let completeString = self + suffix
let size = font.sizeOfString(completeString, constrainedToWidth: width)
if size.height > lineHeight * CGFloat(lines) {
let partialString = self.components(separatedBy: " ").dropLast().joined(separator: " ")
return partialString.textThatFits(width: width, lines: lines, font: font, suffix: suffix)
} else {
return completeString
}
}
}
extension UIFont {
/**
Calculate the height of a string with this font and constrained width
- Parameter string: String to calculate size for
- Parameter width: The constrained width for the bounding box when calculating the size
- Returns: Size of string contained in bounding rect of width and max height
*/
func sizeOfString(_ string: String, constrainedToWidth width: Double) -> CGSize {
return (string as NSString).boundingRect(
with: CGSize(width: width, height: .greatestFiniteMagnitude),
options: [.usesFontLeading, .usesLineFragmentOrigin],
attributes: [.font: self],
context: nil).size
}
}
Then I had to call it from viewDidLayoutSubviews since my label's width wasn't correct until then. Lastly, I used this like this:
private func setReadMoreText() {
let readMoreSuffix = "... Read more"
let font = // YOUR_FONT
let fittingString = YOUR_LONG_STRING.textThatFits(width: YOUR_UILABEL.frame.width, lines: NUMBER_OF_LINES, font: font, suffix: readMoreSuffix)
// This is not needed but I wanted the Read more text to be colored. You could just set your fittingString directly as text.
let privacyAttrString = NSMutableAttributedString(string: fittingString, attributes: [.font: font])
privacyAttrString.addAttributes([.foregroundColor: YOUR_COLOR], range: NSRange(location: fittingString.count - readMoreSuffix.count + 4, length: readMoreSuffix.count - 4))
self.YOUR_UILABEL.text = privacyAttrString
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow