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
相关产品推荐
相关产品推荐

