You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中如何在UITextView中实现文本中间截断

UITextView单行模式实现消息文本中间截断

需求场景

  • 多行模式:正常展示「标题+长消息+操作按钮」三段文本,无问题
  • 单行模式:当前使用NSLineBreakMode.byTruncatingTail会从整体文本尾部截断,期望仅截断中间的消息(Message)部分,保留标题和尾部操作按钮完整显示

现有实现代码

@IBAction func btnSwitchAction(_ sender: UISwitch) {
    self.isMultilineEnabled = sender.isOn
}

func setup() {
    textView.isScrollEnabled = false
    textView.isEditable = false
    reloadTextView()
}

func reloadTextView() {
    let padding = textView.textContainer.lineFragmentPadding
    textView.textContainerInset = UIEdgeInsets(top: 0, left: -padding, bottom: 0, right: -padding)
    textView.textContainer.maximumNumberOfLines =  isMultilineEnabled ? 0 : 1;
    textView.textContainer.lineBreakMode = isMultilineEnabled ? NSLineBreakMode.byWordWrapping : NSLineBreakMode.byTruncatingTail
    textView.attributedText = getAttributedString(title: "Des", messageStr: "In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used as a placeholder", actionButton: "Share")
    textView.layoutIfNeeded()
    textView.translatesAutoresizingMaskIntoConstraints = true
    textView.sizeToFit()
}

private func getAttributedString(title: String, messageStr: String, actionButton: String = "") -> NSAttributedString {
    let defaultFont = UIFont.systemFont(ofSize: fontSize)
    let boldFont = UIFont.systemFont(ofSize: fontSize, weight: .bold)
   
    let finalAttributedString = NSMutableAttributedString.init(string: "")
    
    let firstText = "\(title):"
    let regulerAttributedText = NSMutableAttributedString(string: firstText,
                                                          attributes: [NSAttributedString.Key.font : boldFont])
    finalAttributedString.append(regulerAttributedText)

    let secondAttributedText = NSMutableAttributedString(string: messageStr,
                                                          attributes: [NSAttributedString.Key.font : defaultFont])
    finalAttributedString.append(secondAttributedText)
    finalAttributedString.addAttributes([NSAttributedString.Key.foregroundColor : UIColor.black.cgColor], range: NSMakeRange(0, finalAttributedString.string.count))
            
    if !actionButton.isEmpty {
        let actionFont = UIFont.systemFont(ofSize: fontSize, weight: .medium)
        let actionTextAttribute = NSMutableAttributedString(string: " \(actionButton)")
        actionTextAttribute.addAttributes([NSAttributedString.Key.foregroundColor : UIColor.orange.cgColor,
                                            NSAttributedString.Key.font : defaultFont],
                                          range: NSMakeRange(0, actionButton.count + 1))
        finalAttributedString.append(actionTextAttribute)
    }
    return finalAttributedString
}

尝试过的无效方法

曾尝试通过NSLayoutManager扩展获取单行可容纳的字符范围,但无法得到正确结果:

extension NSLayoutManager {
    /**
     Returns characters range that completely fits into container.
     */
    public func characterRangeThatFits(textContainer container: NSTextContainer) -> NSRange {
        var rangeThatFits = self.glyphRange(for: container)
        rangeThatFits = self.characterRange(forGlyphRange: rangeThatFits, actualGlyphRange: nil)
        return rangeThatFits
    }
 }

解决方案:手动计算宽度实现消息中间截断

核心思路:单行模式下,先计算标题和操作按钮的占用宽度,得出消息文本的可用宽度,再对消息文本进行中间截断处理,最后拼接成完整富文本。

1. 新增中间截断处理方法

private func truncatedMiddleString(for string: String, availableWidth: CGFloat, font: UIFont) -> String {
    let ellipsis = "..."
    let ellipsisWidth = ellipsis.size(withAttributes: [.font: font]).width
    
    // 用二分法找到左侧可容纳的最长子串
    var leftIndex = string.startIndex
    var rightIndex = string.endIndex
    while leftIndex < rightIndex {
        let middleIndex = string.index(leftIndex, offsetBy: string.distance(from: leftIndex, to: rightIndex)/2)
        let leftPartWidth = String(string[..<middleIndex]).size(withAttributes: [.font: font]).width
        
        if leftPartWidth + ellipsisWidth < availableWidth {
            leftIndex = middleIndex
        } else {
            rightIndex = middleIndex
        }
    }
    
    let leftPart = String(string[..<leftIndex])
    let remainingWidth = availableWidth - leftPart.size(withAttributes: [.font: font]).width - ellipsisWidth
    
    // 从尾部往前找右侧可容纳的最长子串
    rightIndex = string.endIndex
    var rightPart = ""
    while rightIndex > leftIndex {
        let prevIndex = string.index(before: rightIndex)
        let testRightPart = String(string[prevIndex...])
        let testWidth = testRightPart.size(withAttributes: [.font: font]).width
        
        if testWidth <= remainingWidth {
            rightPart = testRightPart
            break
        }
        rightIndex = prevIndex
    }
    
    return leftPart + ellipsis + rightPart
}

2. 修改reloadTextView方法

在单行模式下调用截断方法处理消息文本:

func reloadTextView() {
    let padding = textView.textContainer.lineFragmentPadding
    textView.textContainerInset = UIEdgeInsets(top: 0, left: -padding, bottom: 0, right: -padding)
    textView.textContainer.maximumNumberOfLines = isMultilineEnabled ? 0 : 1
    textView.textContainer.lineBreakMode = isMultilineEnabled ? .byWordWrapping : .byTruncatingTail
    
    let title = "Des"
    let originalMessage = "In publishing and graphic design, Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content. Lorem ipsum may be used as a placeholder"
    let actionButton = "Share"
    
    let boldFont = UIFont.systemFont(ofSize: fontSize, weight: .bold)
    let defaultFont = UIFont.systemFont(ofSize: fontSize)
    var attributedText: NSAttributedString
    
    if isMultilineEnabled {
        attributedText = getAttributedString(title: title, messageStr: originalMessage, actionButton: actionButton)
    } else {
        // 计算标题和操作按钮的宽度
        let titleWidth = "\(title):".size(withAttributes: [.font: boldFont]).width
        let actionWidth = " \(actionButton)".size(withAttributes: [.font: defaultFont]).width
        let textViewUsableWidth = textView.bounds.width - 2 * padding
        
        // 消息文本的可用宽度
        let availableMessageWidth = textViewUsableWidth - titleWidth - actionWidth
        
        // 处理消息中间截断
        let truncatedMessage = truncatedMiddleString(for: originalMessage, availableWidth: availableMessageWidth, font: defaultFont)
        attributedText = getAttributedString(title: title, messageStr: truncatedMessage, actionButton: actionButton)
    }
    
    textView.attributedText = attributedText
    textView.layoutIfNeeded()
    textView.translatesAutoresizingMaskIntoConstraints = true
    textView.sizeToFit()
}

方案优势

  • 精准计算各部分文本宽度,确保标题和操作按钮在单行模式下完整显示
  • 采用二分法高效查找可容纳的文本片段,性能优于逐字符遍历
  • 兼容富文本的字体样式,避免因字体差异导致的宽度计算误差

内容的提问来源于stack exchange,提问作者Dhiru

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 16:07:08