Add "...Read More" to the end of UILabel
Asked Answered
B

19

26

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!

Battleax answered 31/8, 2015 at 10:33 Comment(1)
use truncateinMiddleWiencke
B
13

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!

Battleax answered 1/10, 2015 at 8:1 Comment(7)
How to increase the label hieght when tap on lableViolette
please give me swift version of this answer.Unmade
Tap Gesture will work on whole label instead of just Read More text.Skipper
can I get the swift version of this functionOptime
Swift version please??Hypogynous
Anybody got swift version?Clericalism
How to give action for "add more" MDHTMLLabel ? I want to load html string and add more option.Kwangju
T
33

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

Readmore label output

NOTE: - Action is not included for Readmore

Tether answered 19/5, 2017 at 7:53 Comment(8)
how can we add an action?Trigger
@JesseOnolememen you can add action on label that would be valid tooEntirely
I am getting Cannot convert value of type '[String : Any]?' to expected argument type '[NSAttributedStringKey : Any]?' with Swift 4Hypogynous
BE CAREFUL! It works as long as the string is a certain size otherwise it will crash. For example it won't crash if the text is "managers" but it WILL CRASH if the text is "manager". It will also crash if the text is "abcdefg" or "......." (there are 7 dots there). Every app is different and for mines I let the user type any text from 3 - 100 characters. The font I used is UIFont(name: "ArialRoundedMTRegular", size: 12). Test the char limit if your using a different font and test using simple one syllable words to see if this crashes. Other then that then this works fine.Ballerina
The problem is if the # of text chars is less then the append text there will be a crash. For eg instead of using '... Readmore' I used '... more' which is 8 chars including the space. The word "manager" and "abcdefg" is 7 chars which is less then 8 so it crashed. I changed the appended text to say '...' which is only 3 char and it worked fine. Basically number of chars in the label text has to be >= the number of chars in the with: and moreText: parameters.Ballerina
The exception is: Terminating app due to uncaught exception 'NSRangeException', reason: '-[__NSCFString replaceCharactersInRange:withString:]: Range or index out of bounds'. >>> The crash happens on this line: let trimmedForReadMore: String = (trimmedString! as NSString).replacingCharacters(in: NSRange(location: ((trimmedString?.count ?? 0) - readMoreLength), length: readMoreLength), with: "") + trailingText.Ballerina
this is too buggy and addresses only a specific scenario. if you are using nsattributedstring, it gets tricky and you gotta make adjustments. if you have nstextattachments, then its gg.Extrapolate
If the text is contained in a tableView (and presumably in a collectionView) then any update on the same cell will remove the ... ReadMore button and after some time the thread places it again.Trove
B
13

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!

Battleax answered 1/10, 2015 at 8:1 Comment(7)
How to increase the label hieght when tap on lableViolette
please give me swift version of this answer.Unmade
Tap Gesture will work on whole label instead of just Read More text.Skipper
can I get the swift version of this functionOptime
Swift version please??Hypogynous
Anybody got swift version?Clericalism
How to give action for "add more" MDHTMLLabel ? I want to load html string and add more option.Kwangju
B
11

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
        }
    }
}
Ballerina answered 22/3, 2019 at 3:22 Comment(5)
Thanks Lance for making it better. I am using this code. It is not showing the text in Label. I didn't make any modification, I am using it as it is.Dastardly
let readmoreFont = UIFont(name: "Helvetica-Oblique", size: 14.0) let readmoreFontColor = UIColor.blue DispatchQueue.main.async { self.movieDescLbl.addTrailing(with: self.movieDesc, moreText: "Readmore", moreTextFont: readmoreFont!, moreTextColor: readmoreFontColor) }Dastardly
Let us continue this discussion in chat.Dastardly
self.movieDescLbl.addTrailing(with: self.movieDesc, moreText: "Readmore", moreTextFont: readmoreFont, moreTextColor: readmoreFontColor). Here self.movieDesc is a string with more than 40 characters in it. so i am doing right or wrong ?Dastardly
@Raxit Pandya I see your problem, your doing it wrong, look at what you wrote man, it's a simple mistake. The problem is here addTrailing(with: self.movieDesc, it's supposed to be addTrailing(with: "..." Your supposed to add 3 dots thereBallerina
D
10

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
Devisor answered 31/8, 2015 at 19:13 Comment(0)
L
7

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

enter image description here

Result- After tapping ReadMore

enter image description here

Lubricate answered 8/7, 2019 at 14:0 Comment(5)
Thank you for full answer, It worked for me except this part : if let _ = font { self.font = font } because we are assigning the text to label's `attributedText' property we need to add font as attribute to stringsBay
I just updated as per my requirement and its working like charm. Thank youTrouper
thanks bro, thats what exactly what i was looking forVineyard
@MohyG, how did you manage to set font in the showTextOnTTTAttributeLable function? I've tried setting up the fonts in attributes but except Read More and Read Less, whole text comes up as black and in a very small font size. Any help will be greatly appreciated!Pogrom
Refer this gist.github.com/Catherine-K-George/…Soapsuds
Z
3

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

Zionism answered 19/5, 2016 at 2:40 Comment(1)
This only works when you want "Read more" option below the UILabel and not next to it, right?Solothurn
F
3
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.

Flaunty answered 22/9, 2019 at 2:11 Comment(0)
L
2

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.

Loose answered 31/8, 2015 at 10:39 Comment(1)
I've calculated the bounding rect of the original text (as an NSAttributedString), what should I do next? how can I check how many characters will be fit into this size and trunk the text and substring it with the '...Read More' text?Battleax
G
2

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.

Gemmule answered 26/10, 2016 at 8:45 Comment(3)
Only working for System font, not working for Custom font.Jeddy
Actually I am using it with custom font, weight and size. It works perfectly, you just need to usit in NSAttributedString(string:attributes:) function.Stopwatch
i have an issue with this library, in tableview 'Show more' is not showing when tableview loads first time after a scroll it shows againNairobi
C
1

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
}
Cowlick answered 13/2, 2022 at 11:36 Comment(0)
H
0

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'...");

}
}
Hileman answered 14/9, 2017 at 10:51 Comment(0)
J
0
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
    }
}
Jacquelynejacquelynn answered 2/5, 2018 at 6:48 Comment(0)
A
0

For Action

 let tap = UITapGestureRecognizer(target: self, action: #selector(self.tapFunction))
 Urlabel.isUserInteractionEnabled = true
 Urlabel.addGestureRecognizer(tap)

 @objc
    func tapFunction(sender:UITapGestureRecognizer) {


    }
Autograft answered 28/2, 2019 at 17:10 Comment(0)
H
0

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.

Hostage answered 11/8, 2019 at 20:5 Comment(0)
L
0

Here is another solution with Swift 5.

Reference

Result

enter image description here

Steps

Logic is simple.

  1. check if the label's text is truncated or not.
  2. get an index of the starting point of ellipsis in the label's text using LayoutManager
  3. slice the text using the index of ellipsis
  4. replace the substrings with ... more (You can change it)
  5. add UITapGesture on the label
  6. get the index in the label's attributedString from gestrure location -> private func getIndex(from point: CGPoint) -> Int?
  7. 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://mcmap.net/q/412094/-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://mcmap.net/q/99746/-create-tap-able-quot-links-quot-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
    }
}


Lardon answered 8/9, 2020 at 16:21 Comment(1)
how to use it, i did not understandBankroll
B
0

This is the best answer for me but is missing the tap to show more text: https://mcmap.net/q/410145/-add-quot-read-more-quot-to-the-end-of-uilabel

let tap = UITapGestureRecognizer(target: self, action: #selector(self.showMore(_:)))
    subTitleLabel.isUserInteractionEnabled = true
    subTitleLabel.addGestureRecognizer(tap)

and you can switch between full text and less text by changing the number of lines but unfortunately, the 0 lines do not work so you have to set a specific number.

@objc func showMore(_ sender: UITapGestureRecognizer? = nil) {
    subTitleLabel.numberOfLines = subTitleLabel.numberOfLines == 3 ? 20 : 3
}
Borrell answered 2/10, 2022 at 7:25 Comment(0)
A
0

I had the need for similar functionality, and came up with an approach I didn't see documented anywhere. I wrote it up as a blog post, but I'll copy the salient parts here.

Note that I'm doing custom TextKit drawing already -- that is, I have a TextKit stack, and am rendering it into a custom view by using the drawGlyphs(forGlyphRange:, at:) method.

Cache the vertical position of the last line fragment rect

I was already doing an initial layout pass (with a larger height) during the height calculation code for my view. My view had a target height, and then it would find the last line that would fit completely into that target height, and set the view’s actual height to the bottom edge of that line. (This ensured no half-cut-off lines.)

I just added caching of the position of that last line, so that I could refer to it during TextKit layout.

public class MyCustomTextKitView: UIView {

  ...

  @objc public func setTextContainerSize(forWidth width: CGFloat, targetHeight: CGFloat) {
    // 1. set text container to the size with maximum height
    textContainer.size = CGSize(width: width - textContainerInsets.left - textContainerInsets.right, height: 1000000)

    // 2. get line fragment that contains the target height
    var previousLineFragmentRect: CGRect = CGRect.zero
    let targetTextContainerHeight = targetHeight - textContainerInsets.top - textContainerInsets.bottom

    layoutManager.ensureLayout(for: textContainer)
    layoutManager.enumerateLineFragments(forGlyphRange: layoutManager.glyphRange(for: textContainer)) { currentLineFragmentRect, usedRect, inTextContainer, glyphRange, stopPointer in
      // Check if target height was inside this line
      if currentLineFragmentRect.maxY > targetHeight {
        stopPointer.initialize(to: true)
        return
      }
      previousLineFragmentRect = currentLineFragmentRect
    }

    let prevLineFragmentMaxY = previousLineFragmentRect.maxY
    var targetTextContainerSize = CGSize.zero

    // 3. set text container size and cache the height of last line fragment rect
    targetTextContainerSize = CGSize(width: width - textContainerInsets.left - textContainerInsets.right, height: prevLineFragmentMaxY + textContainerInsets.top + textContainerInsets.bottom)

    textContainer.size = targetTextContainerSize
    layoutManager.activeTruncationMode = .truncateLine(previousLineFragmentRect) // this variable is in my custom subclass of NSLayoutManager
  }
}

Subclass NSTextContainer to modify the size of the last line fragment rect

public class TextContainer: NSTextContainer {

  override public func lineFragmentRect(
    forProposedRect proposedRect: CGRect,
    at characterIndex: Int,
    writingDirection baseWritingDirection: NSWritingDirection,
    remaining remainingRect: UnsafeMutablePointer<CGRect>?
  ) -> CGRect {
    var lineFragmentRect = super.lineFragmentRect(forProposedRect: proposedRect,
                                      at: characterIndex,
                                      writingDirection: baseWritingDirection,
                                      remaining: remainingRect)

    guard let layoutManager = layoutManager as? LayoutManager,
          case let .truncateLine(desiredTruncationLine) = layoutManager.activeTruncationMode,
          let truncationString = layoutManager.customTruncationString
    else {
      return lineFragmentRect
    }

    // check if we're looking at the last line
    guard lineFragmentRect.minX == desiredTruncationLine.minX else {
      return lineFragmentRect
    }

    // we have a match, and should truncate. Shrink the line by enough room to display our truncation string.
    let truncationAttributes = layoutManager.editor?.getTheme().truncationIndicatorAttributes ?? [:]
    let truncationAttributedString = NSAttributedString(string: truncationString, attributes: truncationAttributes)

    // assuming we don't make the line fragment rect bigger in order to fit the truncation string
    let requiredRect = truncationAttributedString.boundingRect(with: lineFragmentRect.size, options: .usesLineFragmentOrigin, context: nil)

    let spacing = 6.0 // TODO: derive this somehow

    // make the change
    lineFragmentRect.size.width = min(lineFragmentRect.width, size.width - (requiredRect.width + spacing))

    return lineFragmentRect
  }

}

Calculate the location to draw the string in the NSLayoutManagerDelegate

It turned out I had to use both the NSTextContainer method and this one. In the NSTextContainer method above, we shortened the line fragment rect for the last line, to the largest possible size it could be while still accommodating our custom truncation string. But we don’t yet know how much of that line has been used. For example, if it’s the last line of a paragraph, and only one word is on the line, then only a small amount of this possible horizontal width will have been used. (But we’d still need truncation because there may be subsequent paragraphs.)

Enter NSLayoutManagerDelegate. Here we find a method that gives us the data we need. So in this method, we position where we are going to draw our string, and cache the calculated value, ready to draw it later.

class LayoutManagerDelegate: NSObject, NSLayoutManagerDelegate {

  func layoutManager(
    _ layoutManager: NSLayoutManager,
    shouldSetLineFragmentRect lineFragmentRectPointer: UnsafeMutablePointer<CGRect>,
    lineFragmentUsedRect lineFragmentUsedRectPointer: UnsafeMutablePointer<CGRect>,
    baselineOffset: UnsafeMutablePointer<CGFloat>,
    in textContainer: NSTextContainer,
    forGlyphRange glyphRange: NSRange
  ) -> Bool {
    guard let layoutManager = layoutManager as? LayoutManager,
            case let .truncateLine(desiredTruncationLine) = layoutManager.activeTruncationMode,
            let truncationString = layoutManager.customTruncationString
    else {
      return false
    }

    let lineFragmentRect: CGRect = lineFragmentRectPointer.pointee
    let lineFragmentUsedRect: CGRect = lineFragmentUsedRectPointer.pointee

    // check if we're looking at the last line
    guard lineFragmentRect.minX == desiredTruncationLine.minX else {
      return false
    }

    // I should really refactor this code out, as it's used both here and in the TextContainer.
    let truncationAttributes = ...
    let truncationAttributedString = NSAttributedString(string: truncationString, attributes: truncationAttributes)
    let requiredRect = truncationAttributedString.boundingRect(with: lineFragmentRect.size, options: .usesLineFragmentOrigin, context: nil)
    let spacing = 6.0 // TODO: derive this somehow

    // Derive the rect for drawing our custom string, based on the lineFragmentUsedRect, and cache it on the layout manager.
    layoutManager.customTruncationDrawingRect = CGRect(x: lineFragmentUsedRect.width + spacing,
                                                       y: lineFragmentUsedRect.minY + (lineFragmentUsedRect.height - requiredRect.height),
                                                       width: requiredRect.width,
                                                       height: requiredRect.height)

    // we didn't change anything so always return false
    return false
  }

}

Do the drawing in our NSLayoutManager subclass

We’ve now adjusted the line fragment rect so that TextKit will leave a blank space for us. We’ve calculated the rect in which we want to draw our custom string. Now we need to actually draw it. Here’s how.

internal enum ActiveTruncationMode {
  case noTruncation
  case truncateLine(CGRect) // the rect is the pre-calculated last line fragment rect
}

public class LayoutManager: NSLayoutManager {
  public var customTruncationString: String? = "See More"
  internal var activeTruncationMode: ActiveTruncationMode = .noTruncation
  internal var customTruncationDrawingRect: CGRect?

  override public func drawGlyphs(forGlyphRange drawingGlyphRange: NSRange, at origin: CGPoint) {
    super.drawGlyphs(forGlyphRange: drawingGlyphRange, at: origin)
    drawCustomTruncationIfNeeded(forGlyphRange: drawingGlyphRange, at: origin)
  }

  private func drawCustomTruncationIfNeeded(forGlyphRange drawingGlyphRange: NSRange, at origin: CGPoint) {
    guard let customTruncationString = customTruncationString,
            let customTruncationDrawingRect = customTruncationDrawingRect,
            let attributes = ... else { return }

    let modifiedDrawingRect = customTruncationDrawingRect.offsetBy(dx: origin.x, dy: origin.y)
    let attributedString = NSAttributedString(string: customTruncationString, attributes: attributes)
    attributedString.draw(in: modifiedDrawingRect)
  }

}

And that’s that! All together, this code handles truncation in just the way I wanted.

Adinaadine answered 27/11, 2022 at 18:49 Comment(1)
Do you have any example working snippet for the same? Like in git?Suspiration
V
-1

TTTAttributedLabel:- Use below line of code to set font

attributedLessText = NSMutableAttributedString(string: subStringWithReadMore, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17)])

Vallee answered 20/8, 2020 at 8:4 Comment(1)
Welcome to Stack Overflow. Code-only answers are discouraged on Stack Overflow because they don't explain how it solves the problem. Please edit your answer to explain how this answers the question and how it improves on the in the existing answers, so that it is useful to other users with similar issues.Honeywell
M
-4

Do you know there is no touch action of UILabel. so you cant touch '...Read More' if whole text in a UILabel.

Note: my solution is, add a clear background button end of the UILabel.

Myosin answered 31/8, 2015 at 10:45 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.