Changing the text of an AttributedString is remarkably tricky. You have to replace the contents of the attributed string's character view — its characters
property. To make things even harder, you cannot do this simply by assigning another string!
Using our button as an example, this won't compile:
button.configuration?.attributedTitle?.characters = "Goodbye" // error
Nor is it sufficient to derive the character view from a simple string. This doesn't compile either:
button.configuration?.attributedTitle?.characters = "Goodbye".characters // error
This is because the separate character view of a simple string no longer exists; you're still trying to assign a String into a character view, and we already know you can't do that.
Instead, you can make an AttributedString.CharacterView directly from your String and assign that into the target attributed string's characters
property. Swift inferred typing is a big help here:
button.configuration?.attributedTitle?.characters = .init("Goodbye")
That replaces the button's title without disturbing the button's title's style attributes.
More about buttons
Replacing a button's title is such a useful thing to be able to do that I've made a little utility extension on UIButton that covers all the cases — a button that isn't configuration-based, a button that is configuration-based but has no attributed title, and a button that is configuration-based and has an attributed title:
extension UIButton {
func replaceTitle(_ newTitle: String) {
guard configuration != nil else {
setTitle(newTitle, for: .normal)
return
}
guard configuration?.attributedTitle != nil else {
configuration?.title = newTitle
return
}
configuration?.attributedTitle?.characters = .init(newTitle)
}
}
Even more about buttons
It may be (and has been) asked, in the case of the configuration-based button, why use the attributedTitle
at all? Why not set the font in the button configuration's buttonConfig.titleTextAttributesTransformer
?
That answer is that that doesn't work for real-life fonts that need to respond dynamically to the user changing their text size preference. To see what I mean, try this example:
let button = UIButton(configuration: .plain())
let font = UIFontMetrics(forTextStyle: .subheadline)
.scaledFont(for: UIFont(name: "Georgia", size: 16)!)
button.configuration?.title = "Hello"
button.configuration?.titleTextAttributesTransformer = .init { container in
container.merging(AttributeContainer.font(font))
}
You will see that, although the button title appears initially in the correct font, and although the font survives setting the button's configuration title, the dynamism of the font size has been lost. With a dynamically sized font, you must set the font as part of the attributed title.
This is, in fact, the use case that ultimately inspired my original question, and hence this answer.