Created
June 23, 2017 19:37
-
-
Save imownbey/1ff76acb5517e754f690a0b012fa543c to your computer and use it in GitHub Desktop.
Truncate a AttributedString with something other than just "..."
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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)