A clean way to achieve this is to extend String
so it can provide a 'wrapped' version of itself, and then use this in a subclass of UILabel
to keep things clean at the point of use.
So extend String
to wrap itself into a multi-line string at a certain character width:
extension String {
func wrap(at width: Int) -> String {
return self
.indices
.enumerated()
.reduce(""){
let charAsString = String(self[$1.1])
let position = $1.0
guard position != 0 else {return charAsString}
if position.isMultiple(of: width) {
return $0 + "\n" + charAsString
} else {
return $0 + charAsString
}
}
}
}
A couple of things of note:
- you need to use the original indices, not the just the length of the produced string or it's indices as adding the line break affects the number of characters
- you actually want to wrap the original string, i.e. insert the line break, every
width + 1
characters. Helpfully sequence enumerations are 0-indexed so you get the +1
for free :)
- you could put the whole
reduce
closure into a single line ternary operation. I did initially and it was hideous to read, so an if...else
is far more maintainable.
Once this is in place, creating the custom UILAbel
is pretty straightforward. Create a subclass and override the text
property:
class WrappedLabel: UILabel {
let width: Int
init(withWidth width: Int, frame: CGRect = .zero) {
self.width = width
super.init(frame: frame)
numberOfLines = 0 //to allow auto-sizing of the label
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var text: String? {
set {
guard let newValue = newValue else {return}
super.text = newValue.wrap(at: width)
}
get {
super.text
}
}
}
Implementation is then as simple as
let label = WrappedLabel(withWidth: 20)