Skip to content

Instantly share code, notes, and snippets.

@imownbey
Created June 23, 2017 19:37
Show Gist options
  • Save imownbey/1ff76acb5517e754f690a0b012fa543c to your computer and use it in GitHub Desktop.
Save imownbey/1ff76acb5517e754f690a0b012fa543c to your computer and use it in GitHub Desktop.
Truncate a AttributedString with something other than just "..."
import Foundation
import UIKit
extension NSAttributedString {
// Get the number of lines an attributed string takes
func lineCount(atSize size: CGSize) -> Int {
let attrs = self.attributes(at: 0, effectiveRange: nil)
guard let font = (attrs[NSFontAttributeName] as? UIFont) else {
return 0
}
let paragraph = attrs[NSParagraphStyleAttributeName] as? NSParagraphStyle
let fontMultiplyer = paragraph?.lineHeightMultiple ?? 1.0
let lineSpacing = paragraph?.lineSpacing ?? 0
// Take the font's lineHeight, multiply it by the lineHeightMultiplyer and add lineSpacing
// (Linespacing is maybe wrong here?) to get height of a single line
let singleLineHeight = ceil(font.lineHeight * fontMultiplyer + lineSpacing)
// Get our own text height
let textHeight = self.boundingRect(with: size, options: .usesLineFragmentOrigin, context: nil).height
return Int(ceil(textHeight / singleLineHeight))
}
func truncated(withAttrString truncation: NSAttributedString, atLine numLines: Int, width: CGFloat) -> NSAttributedString {
if lineCount(atSize: CGSize(width: width, height: CGFloat.infinity)) <= numLines {
// Smaller than number of lines we care about
return self
}
var subAttr = NSMutableAttributedString()
var lastEnd: Int = 0
// 1. Enumerate through words
(self.string as NSString).enumerateSubstrings(in: NSMakeRange(0, self.string.characters.count), options: .byWords) { (substr, strRange, strRangeWithEnd, stop) in
// 2. Add the word (without the whitespace) and the truncation string (ie. "... more") to the str
subAttr.append(self.attributedSubstring(from: strRange))
subAttr.append(truncation)
// 3. See if that attributed string is too many lines
if subAttr.lineCount(atSize: CGSize(width: width, height: CGFloat.infinity)) > numLines {
// 3a. If it is then delete the last word (and any whitespace before it) but leave the truncation string
// We need to delete to the end of the previous range (and then add the difference to the length of the range) because there can be multiple whitespace characters together
subAttr.deleteCharacters(in: NSMakeRange(lastEnd, strRange.length + (strRange.location - lastEnd)))
stop.pointee = true
} else {
// 3b. If it is too short track the end of this (for sake of deleting whitespace on next pass),
// delete the word from the end and then add the word + whitespace and go back to #2
lastEnd = strRange.location + strRange.length
let deleteRange = NSMakeRange(strRange.location, strRange.length + truncation.length)
subAttr.deleteCharacters(in: deleteRange)
subAttr.append(self.attributedSubstring(from: strRangeWithEnd))
}
}
return subAttr
}
}
@tudk
Copy link

tudk commented Dec 24, 2020

This will be crash if NSAttributedString.string has emoji.

I think this can be changed as bellow

(self.string as NSString).enumerateSubstrings(in: NSMakeRange(0, self.string.characters.count), options: .byWords) { (substr, strRange, strRangeWithEnd, stop)
-->
(self.string as NSString).enumerateSubstrings(in: NSMakeRange(0, self.string.characters.count), options: .byComposedCharacterSequences) { (substr, strRange, strRangeWithEnd, stop)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment