skip to Main Content

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
        }
}

Edit: 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.)

Can anyone suggest how to 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. Thanks for any suggestions.

2

Answers


  1. The problem here is that "boldification" is not a thing. Given a font Georgia, you cannot simply boldify it; you have to know that the bold variant is a different font, Georgia-Bold. To find that out, or the equivalent, you have to pass thru UIFontDescriptor. Here’s a couple of extensions that might let you find the bold variant of a given font:

    extension UIFontDescriptor {
        func boldVariant() -> UIFontDescriptor? {
            var traits = self.symbolicTraits
            traits.insert(.traitBold)
            return self.withSymbolicTraits(traits)
        }
    }
    
    extension UIFont {
        func boldVariant() -> UIFont? {
            if let descriptor = self.fontDescriptor.boldVariant() {
                return UIFont(descriptor: descriptor, size: 0)
            }
            return nil
        }
    }
    

    Once you do know that, then here’s a working extension that lets you apply an attribute container to a substring of an attributed string:

    protocol Configurable {}
    
    extension Configurable where Self: Any {
        func configured(with handler: (inout Self) -> Void) -> Self {
            var copy = self
            handler(&copy)
            return copy
        }
    }
    
    extension AttributedString: Configurable {}
    
    extension AttributedString {
        func applying(_ attributes: AttributeContainer, to text: any StringProtocol) -> Self {
            guard let range = range(of: text) else { return self }
            return self.configured {
                $0[range].mergeAttributes(attributes)
            }
        }
    }
    

    That’s more general than what you asked for, but in my opinion, generality is better. You can always add more code to pick out a specific case in point. Anyway, here’s an example of how to use it (which is, after all, the whole point):

    let font = UIFont(name: "Georgia", size: 12)!
    var text = AttributedString(
        "Hello, world!",
        attributes: AttributeContainer.font(font)
    )
    if let boldifiedFont = font.boldVariant() {
        text = text.applying(
            AttributeContainer.font(boldifiedFont),
            to: "world"
        )
    }
    

    That boldifies just the "world" part of "Hello, world!"

    Login or Signup to reply.
  2. 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)
        }
    }
    
    
    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") )
            }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search