You can't get a set number of characters in a row with every font since some fonts have characters with variable widths. What you can do is try to break the string up into line-sized chunks:
extension String {
func split(width: CGFloat, font: UIFont) -> [String] {
guard !self.isEmpty else { return [String]() }
var lines = [String]()
// set up range of the split
var splitStart = self.startIndex
var splitEnd = self.startIndex
repeat {
// advance the end range for the split
splitEnd = self.index(after: splitStart)
// initial split to test
var line = String(characters[splitStart..<splitEnd])
// while we're before the end test the rendered width
while splitEnd < self.endIndex &&
line.size(attributes: [NSFontAttributeName: font]).width < width {
// add one more character
splitEnd = self.index(after: splitEnd)
line = String(characters[splitStart..<splitEnd])
}
// add split to array and set up next split
lines.append(line)
splitStart = splitEnd
} while splitEnd < self.endIndex // don't go past the end of the string
// add remainder of string to array
lines.append(String(characters[splitStart..<self.endIndex]))
return lines
}
}
This can be optimized a bit by pre-calculating the width of the entire string, dividing by the number of rows, starting with the average width for each line and then try more or less characters until it fits. However, that does make the code much more complex.
If you want to make sure that words don't get split up then you could save the position of the start of each word and when you reach the end of a line make the split end before the word, taking the word to the next split. Of course you would want to also account for words that are too long to split, hyphenating words, and so on.
Another way to do it, since you need more advanced layout, would be to use NSLayoutManager
and NSTextContainer
:
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Non est ista, inquam, Piso, magna dissensio. Minime vero istorum quidem, inquit. Graecum enim hunc versum nostis omnes-: Suavis laborum est praeteritorum memoria. Negat enim summo bono afferre incrementum diem. Quasi ego id curem, quid ille aiat aut neget. Semper enim ex eo, quod maximas partes continet latissimeque funditur, tota res appellatur. Duo Reges: constructio interrete."
let font = UIFont.systemFont(ofSize: 24.0)
// set up styled text for the container
let storage = NSTextStorage(string: text, attributes: [NSFontAttributeName: font])
// add a layout manage for the storage
let layout = NSLayoutManager()
storage.addLayoutManager(layout)
// Set up the size of the container
// width is what we care about, height is maximum
let width:CGFloat = 500
let container = NSTextContainer(size: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude))
// add the container to the layout
layout.addTextContainer(container)
var lines = [String]()
// generate the layout and add each line to the array
layout.enumerateLineFragments(forGlyphRange: NSMakeRange(0, storage.length)) {
lines.append(storage.attributedSubstring(from: $0.3).string)
}
lines.forEach { print($0) }
Result:
Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Non est ista, inquam, Piso,
magna dissensio. Minime vero istorum
quidem, inquit. Graecum enim hunc versum
nostis omnes-: Suavis laborum est
praeteritorum memoria. Negat enim summo
bono afferre incrementum diem. Quasi ego id
curem, quid ille aiat aut neget. Semper enim
ex eo, quod maximas partes continet
latissimeque funditur, tota res appellatur. Duo
Reges: constructio interrete.
You can also provide hyphenation and other behaviors through the NSLayoutManager
, if you like.