在 UILabel 末尾添加“...阅读更多”

Add "...Read More" to the end of UILabel

我有一个 UILabel,在某些情况下,文本比 UILabel 本身长,所以我看到的文本是 "bla bla bla..." 我想添加一个 ...Read More 按钮文本在 UILabel..

末尾

我读过一些帖子,但他们提供的解决方案对我来说并不好,例如:计算将输入 UILabel 的字符数,但是我使用的字体每个字符都有不同的宽度。

我怎样才能做到这一点?

提前致谢!

使用方法 - boundingRectWithSize:options:attributes:context: 并将您的字体作为 NSFontAttributeName 键传递给 NSAttributedString 将为您提供所需的正确矩形。

由此您需要检查它是否大于您的标签边界减去偏移量。只有在是的情况下,您才需要 trim 您的文本并在末尾显示 Read More

你知道UILabel没有触摸动作吗?所以如果 UILabel 中的整个文本你不能触摸“...阅读更多”。

注意:我的解决方法是,在UILabel的末尾添加一个清晰的背景按钮。

Tttattributed 标签具有此功能

https://github.com/TTTAttributedLabel/TTTAttributedLabel

您需要将 "truncation" 标记设置为 "read more..."

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

这就是我将 阅读更多... 按钮添加到 UITextViewUITextFieldUILabel 的方法:

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

    }
}

有一个使用fitString:intoLabel的方法可以找到

至于 UITagTapGestureRecognizer 只是一个普通的 UITapGestureRecognizer 子类,带有一个 NSInteger 属性 标签。我这样做是因为我想确定哪些 Read More... 被点击了,如果我在同一个 UIViewController 中点击了多个 Read More...。您可以使用正常的 UITapGestureRecognizer.

尽情享受吧!

我的解决方案是,我在 UILabel 右下角创建一个 UIButton(名称 阅读更多)。 之后我检查 UILabel 是否被截断以显示或隐藏 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 版本

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
    }

添加此扩展程序:

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
 }
}

希望对您有所帮助

你可以试试第三个库ExpandableLabel

将 UILabel 的自定义 class 设置为 ExpandableLabel 并设置所需的行数和折叠文本:

    expandableLabel.numberOfLines = 5
    expandableLabel.collapsedAttributedLink = NSAttributedString(string: "more")
    expandableLabel.ellipsis = NSAttributedString(string: "...")
    // update label expand or collapse state
    expandableLabel.collapsed = true

您可能需要设置 delegate 以在 link 被触摸时收到通知。

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
        }
    }

用法

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)
}

结果

NOTE: - Action is not included for Readmore

这个方法对于showless和show All with up down arrow image很有用: 在标签上添加点击手势

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

}
}
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
    }
}

行动

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

 @objc
    func tapFunction(sender:UITapGestureRecognizer) {


    }

这适用于 Swift 5

这是@ramchandran 答案的更安全版本,因为您不知道用户将输入多少个字符。

在他的回答中,如果用户输入的字符串的长度小于您决定用于 ... Readmore 的任何文本的长度,那么它将崩溃。例如,这就是您的使用方式

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)
    }
}

在上面的例子中,... Readmore 的输出总共是 12 个字符。如果用户输入的字符串是 yourLabel.text = "12345678" 那么字符串的文本将只有 8 个字符。 它会崩溃,因为在下面的行中使用 ((trimmedString?.count ?? 0) - readMoreLength) 的范围会产生负面结果:

// “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

我添加了一个安全检查,以确保如果输入的字符串小于或等于您决定用作 ... Readmore 的字符数,它将 return 和行这将导致崩溃永远不会发生:

// trimmedString is the string the user entered
guard let safeTrimmedString = trimmedString else { return }
if safeTrimmedString.count <= readMoreLength { return }

它位于addTrailing函数的中心

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
        }
    }
}

Swift 4 和 Swift 5。我需要实施相同的。由于已经给出了答案,但根据我的说法,TTTAtributedLabel 是最好的方法。它使您可以更好地控制内容。易于查找地址、link、日期等。您还可以更改 link 的颜色。 TTTAttributedLabel 库 link 已在上面的答案中给出。让我们开始实施吧。

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)
      }
}

我在这里创建了 TTTAttributedLabel 的扩展,并将 ReadMore 和 ReadLess 逻辑放在这里。大家可以根据自己的情况修改。

 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
        }
      }
    }

您需要实现 TTTAttributedLabel 的 didSelectLinkWithTransitInformation 委托。在这里你可以得到你通过的组件

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)
      }
    }
  }
}

结果-点击阅读更多

结果- 点击阅读更多

对于标签上的操作,如果使用 CollectionView 或 TableView,您可以使用委托方法来执行操作。

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()
}

这会更新标签并根据需要显示全文 使用 Lance Samaria 回答并为单元格添加操作。

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
    }

}

通过所有这些混乱的实现只是一个简单的技巧。思路很简单,我们不设置折叠或展开行,只是将标签设置为0。 然后将原始文本存储在 fullText 变量中。现在如果我们想显示折叠格式,那么只需获取子字符串并添加自定义省略号即可。

注意:这不包括点击事件处理程序,您可以在控制器上自行添加。

TTTAttributedLabel:- Use below line of code to set font

attributedLessText = NSMutableAttributedString(字符串: subStringWithReadMore, 属性: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 17)])

这是另一个解决方案 Swift 5.

参考

结果

步骤

逻辑很简单。

  1. 检查标签的文本是否被截断。
  2. 使用 LayoutManager 获取标签文本中省略号起点的索引
  3. 使用省略号索引对文本进行切片
  4. 将子字符串替换为... more(您可以更改)
  5. 在标签上添加 UITapGesture
  6. 从手势位置获取标签属性字符串中的索引 -> private func getIndex(from point: CGPoint) -> Int?
  7. 检查手势位置 -> func didTapInRange(_ point: CGPoint, targetRange: NSRange) -> Bool

示例代码

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])
    }
    
    //
    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
    }
    
    //
    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
    }
}


我在这里找不到任何东西可以工作,所以我发布了我的解决方案。我有一个 UILabel,它应该是 3 行,在文本的末尾它应该是……阅读更多。我制作了一个 UIFont 扩展,用于计算具有特定字体和宽度的字符串的高度,然后我制作了一个 String 扩展,递归地使字符串变小,直到它适合 x 行。

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
}
}

然后我不得不从 viewDidLayoutSubviews 调用它,因为我的标签宽度在那之前不正确。最后,我这样使用:

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
}