I would like to do what would seem like a fairly simple thing that would be trivial in SwiftUI, here on SO, Markdown or HTML: bold a couple words in a string in Swift ideally using an extension for re-use.
For example, bold the word "world" in "Hello world". I would be happy with an extension of any type of String eg String, NSString, NSAttributedString, NSMutableAttributedString and so forth as I can go back and forth. All of the examples I have found--dating typically from a number of years ago--do not compile. For example (with errors commented out):
extension NSMutableAttributedString {
func bold(_ text: String, font: UIFont = .systemFont(ofSize: 17, weight: .bold)) -> Self {
let range = self.range(of: text)!
self.addAttribute(.font, value: font, range: range) //error 'Value of type self has no member range'
return self
}
}
public extension String {
func attributedString(with boldText: String, boldTextStyle: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 17)]) -> NSAttributedString {
let attributedString = NSMutableAttributedString(string: self)
let boldTextRange = range(of: boldText)!
attributedString.addAttributes(boldTextStyle, range: boldTextRange) //Cannot convert value of type 'Range<String.Index>' to expected argument type 'NSRange' (aka '_NSRange')
return attributedString
}
}
One more SO answer in Objective-C not using extension that does compile for me either. Throws about 10 errors:
NSString *normalText = @"Hello ";
NSString *boldText = @"world";
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:normalText];
NSDictionary<NSAttributedString.key, id> *attrs = @{ NSAttributedString.Key.font: [UIFont boldSystemFontOfSize:15] };//throws 5 errors
NSMutableAttributedString *boldString = [[NSMutableAttributedString alloc] initWithString:boldText attributes:attrs];//throws 5 errors
[attributedString appendAttributedString:boldString];
(I have tried many other approaches from old answers and multiple AIs without success.)
How can I get the above to work in modern Swift short of SwiftUI or how to create a string extension to bold a portion of a string?
What the compiler is telling you is that NSMutableAttributedString has no method named range this is true there is no default implementation of any such method on NSMutableAttributedString there is however a property called mutableString that property returns an NSMutableString that does have a function range see below.
Notice I am returning an AttributedString using an init provided by Apple. The reason I'm doing this is because you are using swiftUI and will need something that views like Text and Label will accept.
extension NSMutableAttributedString {
func bold(_ text: String, font: UIFont = .systemFont(ofSize: 17, weight: .bold)) -> AttributedString {
let range = self.mutableString.range(of: text)
self.addAttribute(.font, value: font, range: range)
return try! AttributedString(self, including: \.uiKit)
}
}
here
let boldTextRange = range(of: boldText)!
attributedString.addAttributes(boldTextStyle, range: boldTextRange)
you're being told that func range defined on String returns a Range<String.Index> and in fact it does as per documentation. The simplest repair for that would be to work in either the new world String.Index or the old world NSRange(idx, length) exclusively. Maybe something like below, but please look at my updated answer for what I consider a better approach.
extension String {
func setAttributes(for text: String, attributes: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 17)] ) -> AttributedString {
let container = AttributeContainer.init(attributes)
var attributedString = AttributedString(self)
guard let range = attributedString.range(of: text) else {fatalError()}
attributedString[range].mergeAttributes(container)
return attributedString
}
}
then to use
VStack {
Text("Hello, world")
Text("Hello, world".setAttributes(for: "world"))
Text(NSMutableAttributedString(string: "Hello, world").bold("Hello") )
}