Is there an easy way to get (or simply display) the text from a given line in a UILabel?
My UILabel is correctly displaying my text and laying it out beautifully bu
Very important change regarding iOS 11+
Starting with iOS 11, Apple intentionally changed the behaviour of their word-wrapping feature for UILabel
which effects detecting the String
contents of individual lines in a multiline UILabel
. By design, the word-wrapping of the UILabel
now avoids orphaned text (single words in a new line), as discussed here: word wrapping in iOS 11
Because of that, the way CTFrameGetLines(frame)
returns the CTLine
array of all lines in the label no longer works correctly if the new word-wrapping that avoids orphaned text takes effect in a particular line. To the contrary, it results in parts of the String
that by the new word wrapping design would belong to the next line instead end up in the line in focus.
A tested fix for this problem can be found in my altered version of @TheTiger's answer, which makes use of calculating the actual content size of the UILabel
using sizeThatFits(size:)
, before using that size to create the rect / path written in Swift 4:
extension UILabel {
/// creates an array containing one entry for each line of text the label has
var lines: [String]? {
guard let text = text, let font = font else { return nil }
let attStr = NSMutableAttributedString(string: text)
attStr.addAttribute(NSAttributedString.Key.font, value: font, range: NSRange(location: 0, length: attStr.length))
let frameSetter = CTFramesetterCreateWithAttributedString(attStr as CFAttributedString)
let path = CGMutablePath()
// size needs to be adjusted, because frame might change because of intelligent word wrapping of iOS
let size = sizeThatFits(CGSize(width: self.frame.width, height: .greatestFiniteMagnitude))
path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity)
let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attStr.length), path, nil)
guard let lines = CTFrameGetLines(frame) as? [Any] else { return nil }
var linesArray: [String] = []
for line in lines {
let lineRef = line as! CTLine
let lineRange = CTLineGetStringRange(lineRef)
let range = NSRange(location: lineRange.location, length: lineRange.length)
let lineString = (text as NSString).substring(with: range)
linesArray.append(lineString)
}
return linesArray
}
}
This UILabel
extension returns the contents of the label as a String
array with one entry per line exactly as presented to the eye of the user.