Result:
Swift 4.2 & 5.0:
First off we create a protocol that UILabel
, UITextField
and UITextView
can adopt.
public protocol ChangableFont: AnyObject {
var rangedAttributes: [RangedAttributes] { get }
func getText() -> String?
func set(text: String?)
func getAttributedText() -> NSAttributedString?
func set(attributedText: NSAttributedString?)
func getFont() -> UIFont?
func changeFont(ofText text: String, with font: UIFont)
func changeFont(inRange range: NSRange, with font: UIFont)
func changeTextColor(ofText text: String, with color: UIColor)
func changeTextColor(inRange range: NSRange, with color: UIColor)
func resetFontChanges()
}
We want to be able to add multiple changes to our text, therefore we create the rangedAttributes
property. It's a custom struct that holds attributes and the range in which they are applied.
public struct RangedAttributes {
public let attributes: [NSAttributedString.Key: Any]
public let range: NSRange
public init(_ attributes: [NSAttributedString.Key: Any], inRange range: NSRange) {
self.attributes = attributes
self.range = range
}
}
Another problem is that UILabel
its font
property is strong and UITextField
its font
property is weak/optional. To make them both work with our ChangableFont
protocol we include the getFont() -> UIFont?
method. This also counts for UITextView its text
and attributedText
properties. That's why we implement the getter and setter methods for them as well.
extension UILabel: ChangableFont {
public func getText() -> String? {
return text
}
public func set(text: String?) {
self.text = text
}
public func getAttributedText() -> NSAttributedString? {
return attributedText
}
public func set(attributedText: NSAttributedString?) {
self.attributedText = attributedText
}
public func getFont() -> UIFont? {
return font
}
}
extension UITextField: ChangableFont {
public func getText() -> String? {
return text
}
public func set(text: String?) {
self.text = text
}
public func getAttributedText() -> NSAttributedString? {
return attributedText
}
public func set(attributedText: NSAttributedString?) {
self.attributedText = attributedText
}
public func getFont() -> UIFont? {
return font
}
}
extension UITextView: ChangableFont {
public func getText() -> String? {
return text
}
public func set(text: String?) {
self.text = text
}
public func getAttributedText() -> NSAttributedString? {
return attributedText
}
public func set(attributedText: NSAttributedString?) {
self.attributedText = attributedText
}
public func getFont() -> UIFont? {
return font
}
}
Now we can go ahead and create the default implementation for UILabel
, UITextField
and UITextView
by extending our protocol.
public extension ChangableFont {
var rangedAttributes: [RangedAttributes] {
guard let attributedText = getAttributedText() else {
return []
}
var rangedAttributes: [RangedAttributes] = []
let fullRange = NSRange(
location: 0,
length: attributedText.string.count
)
attributedText.enumerateAttributes(
in: fullRange,
options: []
) { (attributes, range, stop) in
guard range != fullRange, !attributes.isEmpty else { return }
rangedAttributes.append(RangedAttributes(attributes, inRange: range))
}
return rangedAttributes
}
func changeFont(ofText text: String, with font: UIFont) {
guard let range = (self.getAttributedText()?.string ?? self.getText())?.range(ofText: text) else { return }
changeFont(inRange: range, with: font)
}
func changeFont(inRange range: NSRange, with font: UIFont) {
add(attributes: [.font: font], inRange: range)
}
func changeTextColor(ofText text: String, with color: UIColor) {
guard let range = (self.getAttributedText()?.string ?? self.getText())?.range(ofText: text) else { return }
changeTextColor(inRange: range, with: color)
}
func changeTextColor(inRange range: NSRange, with color: UIColor) {
add(attributes: [.foregroundColor: color], inRange: range)
}
private func add(attributes: [NSAttributedString.Key: Any], inRange range: NSRange) {
guard !attributes.isEmpty else { return }
var rangedAttributes: [RangedAttributes] = self.rangedAttributes
var attributedString: NSMutableAttributedString
if let attributedText = getAttributedText() {
attributedString = NSMutableAttributedString(attributedString: attributedText)
} else if let text = getText() {
attributedString = NSMutableAttributedString(string: text)
} else {
return
}
rangedAttributes.append(RangedAttributes(attributes, inRange: range))
rangedAttributes.forEach { (rangedAttributes) in
attributedString.addAttributes(
rangedAttributes.attributes,
range: rangedAttributes.range
)
}
set(attributedText: attributedString)
}
func resetFontChanges() {
guard let text = getText() else { return }
set(attributedText: NSMutableAttributedString(string: text))
}
}
With in the default implementation I use a little helper method for getting the NSRange
of a substring
.
public extension String {
func range(ofText text: String) -> NSRange {
let fullText = self
let range = (fullText as NSString).range(of: text)
return range
}
}
We're done! You can now change parts of the text its font and text color.
titleLabel.text = "Welcome"
titleLabel.font = UIFont.systemFont(ofSize: 70, weight: .bold)
titleLabel.textColor = UIColor.black
titleLabel.changeFont(ofText: "lc", with: UIFont.systemFont(ofSize: 60, weight: .light))
titleLabel.changeTextColor(ofText: "el", with: UIColor.blue)
titleLabel.changeTextColor(ofText: "co", with: UIColor.red)
titleLabel.changeTextColor(ofText: "m", with: UIColor.green)
normalText
to be a different string depending on an index count. See the Update section of my question. Thanks in advance! – Corroboration