3

I have a string let's say " my name is %@ and i study in class %@" now I want to bold the placeholder text which i will be inserting , so that the result will look something like this:" My name is Harsh and i study in class 10" and i will display it on a label

I have already tried using NSAttributedString but since the string will be localised i am not able to use the range parameter of attributed string to make it bold.

Larme
  • 24,190
  • 6
  • 51
  • 81
Harsh Chaturvedi
  • 679
  • 6
  • 13
  • 1
    could you share your code and your localized string? – Maziar Saadatfar Feb 22 '22 at 05:38
  • Did you try markdown? Like adding a double * before and after %@? – HunterLion Feb 22 '22 at 06:22
  • @HunterLion i was trying that but it's not working for me, – Harsh Chaturvedi Feb 22 '22 at 06:35
  • It would be easier to have tags for delimiting the bold part, either Markdown, HTML, or even custom: `[b]`/`[/b]`, ``/``, and once you replace the placeholders values, search for theses tags and render them (bold, italic, etc.) if needed, or use already built in HTML parsing, Markdown one, etc. – Larme Feb 22 '22 at 07:52

3 Answers3

4
let withFormat = "my name is %@ and i study in class %@"

There are different ways to do so, but in my opinion, one of the easiest way would be to use tags:

Use tags around the placeholders (and other parts if needed):

let withFormat = "my name is <b>%@</b> and i study in class <b>%@</b>"
let withFormat = "my name is [b]%@[/b] and i study in class [b]%@[/b]"
let withFormat = "my name is **%@** and i study in class **%@**"

Tags can be HTML, Markdown, BBCode, or any custom you'd like, then, replace the placeholder values:

let localized = String(format: withFormat, value1, value2)

Now, depending on how you want to do it, or which tag you used, you can use the init of NSAttributedString from HTML, Markdown, etc, or simply using NSAttributedString(string: localized), look yourself for the tags and apply the render effect needed.

Here's a little example:

let tv = UITextView(frame: CGRect(x: 0, y: 0, width: 300, height: 130))
tv.backgroundColor = .orange

let attributedString = NSMutableAttributedString()

let htmled = String(format: "my name is <b>%@</b> and i study in class <b>%@</b>", arguments: ["Alice", "Wonderlands"])
let markdowned = String(format: "my name is **%@** and i study in class **%@**", arguments: ["Alice", "Wonderlands"])
let bbcoded = String(format: "my name is [b]%@[/b] and i study in class [b]%@[/b]", arguments: ["Alice", "Wonderlands"])

let separator = NSAttributedString(string: "\n\n")
let html = try! NSAttributedString(data: Data(htmled.utf8), options: [.documentType : NSAttributedString.DocumentType.html], documentAttributes: nil)
attributedString.append(html)
attributedString.append(separator)

let markdown = try! NSAttributedString(markdown: markdowned, baseURL: nil) //iO15+
attributedString.append(markdown)
attributedString.append(separator)

let bbcode = NSMutableAttributedString(string: bbcoded)
let regex = try! NSRegularExpression(pattern: "\\[b\\](.*?)\\[\\/b\\]", options: [])
let matches = regex.matches(in: bbcode.string, options: [], range: NSRange(location: 0, length: bbcode.length))
let boldEffect: [NSAttributedString.Key: Any] = [.font: UIFont.boldSystemFont(ofSize: 12)]
//We use reversed() because if you replace the first one, you'll remove [b] and [/b], meaning that the other ranges will be affected, so the trick is to start from the end
matches.reversed().forEach { aMatch in
    let valueRange = aMatch.range(at: 1) //We use the regex group
    let replacement = NSAttributedString(string: bbcode.attributedSubstring(from: valueRange).string, attributes: boldEffect)
    bbcode.replaceCharacters(in: aMatch.range, with: replacement)
}
attributedString.append(bbcode)

tv.attributedText = attributedString

Output:

enter image description here

Larme
  • 24,190
  • 6
  • 51
  • 81
  • Could you give sample of your full needs, the ones for which solution might not work? – Larme Feb 22 '22 at 08:54
  • 1
    Could you clarify? Because I do not mind `%2$@` or any other position, since the tags are on the string with format, and the rendering is done after and with reading the tags, not the positions... You can test my code by replace `%@` with `%2$@` and `%1$@`, and invert them, it should stil work. – Larme Feb 22 '22 at 08:59
  • Nice and I can see how the reverse was used. – Shawn Frank Feb 22 '22 at 09:14
  • The html one is not scaling with increasing font size in device, how to add different attributes to it?like scalable font etc – Harsh Chaturvedi Feb 22 '22 at 10:52
  • If I understood, you neeed `adjustsFontSizeToFitWidth` & `minimumScaleFactor` of `UILabel`, right? I think that `NSAttributedString` doesn't like them a lot. Or is your issue with initial size? – Larme Feb 22 '22 at 14:21
  • Yeah i want the label text to be scaled with device font size , like doing it for a normal attributed string works but how to do it for html attributed string – Harsh Chaturvedi Feb 23 '22 at 08:54
  • The issue is not with the initial size but the size should change with change in device font size. I was able to use scaled font for nsmutable attributed string, how to do the same here ? – Harsh Chaturvedi Feb 23 '22 at 10:42
  • "The default font for NSAttributedString objects is Helvetica 12-point, which may differ from the default system font for the platform.", so you need to iterate and set the value yourself: https://stackoverflow.com/questions/19921972/parsing-html-into-nsattributedtext-how-to-set-font or https://stackoverflow.com/questions/41412963/swift-change-font-on-an-html-string-that-has-its-own-styles – Larme Feb 23 '22 at 10:49
  • did things change now, bold tag doesnt work – chitgoks Oct 07 '22 at 07:55
  • Define doesn't work: Do you see the `` in the label? Or it's just the rendering that make the text not bold? Did you correctly set `myTextView.attributedText =` and not `myTextView.text =`? Do you override that somewhere? Could you print the `NSAttributedString`? Do you see then that the text in bold (by seeing the "effects that should be applied in text in console")? – Larme Oct 07 '22 at 08:23
0

I can offer a naive solution using NSRegularExpression without any complex / scary regex. I am sure there are more optimal solutions than this.

The steps are quite close to what meaning matters has posted above

  1. Store the string to inject (Harsh, 13) etc in an array
  2. Have a localized string which has placeholders
  3. Use REGEX to find the location of the placeholders and store these locations in a locations array
  4. Update the localized string by replacing the placeholders with values from the string array
  5. Create an NSMutableAttributedString from the updated localized string
  6. Loop through the strings to inject array and update the regions of NSMutableAttributedString defined by the locations array

Here is the code I used with some comments to explain:

// This is not needed, just part of my UI
// Only the inject part is relevant to you
@objc
private func didTapSubmitButton()
{
    if let inputText = textField.text
    {
        let input = inputText.components(separatedBy: ",")
        let text = "My name is %@ and I am %@ years old"
        inject(input, into: text)
    }
}

// The actual function
private func inject(_ strings: [String],
                    into text: String)
{
    let placeholderString = "%@"
    
    // Store all the positions of the %@ in the string
    var placeholderIndexes: [Int] = []
    
    // Locate the %@ in the original text
    do
    {
        let regex = try NSRegularExpression(pattern: placeholderString,
                                            options: .caseInsensitive)
        
        // Loop through all the %@ found and store their locations
        for match in regex.matches(in: text,
                                   options: NSRegularExpression.MatchingOptions(),
                                   range: NSRange(location: 0,
                                                  length: text.count))
            as [NSTextCheckingResult]
        {
            // Append your placeholder array with the location
            placeholderIndexes.append(match.range.location)
        }
    }
    catch
    {
        // handle errors
        print("error")
    }
    
    // Expand your string by inserting the parameters
    let updatedText = String(format: text, arguments: strings)
    
    // Configure an NSMutableAttributedString with the updated text
    let attributedText = NSMutableAttributedString(string: updatedText)
    
        // Keep track of an offset
    // Initially when you store the locations of the %@ in the text
    // My name is %@ and my age is %@ years old, the location is 11 and 27
    // But when you add Harsh, the next location should be increased by
    // the difference in length between the placeholder and the previous
    // string to get the right location of the second parameter
    var offset = 0
    
    // Loop through the strings you want to insert
    for (index, parameter) in strings.enumerated()
    {
        // Get the corresponding location of where it was inserted
        // Plus the offset as discussed above
        let locationOfString = placeholderIndexes[index] + offset
        
        // Get the length of the string
        let stringLength = parameter.count
        
        // Create a range
        let range = NSRange(location: locationOfString,
                            length: stringLength)
        
        // Set the bold font
        let boldFont
            = UIFont.boldSystemFont(ofSize: displayLabel.font.pointSize)
        
        // Set the attributes for the given range
        attributedText.addAttribute(NSAttributedString.Key.font,
                                    value: boldFont,
                                    range: range)
        
        // Update the offset as discussed above
        offset = stringLength - placeholderString.count
    }
    
    // Do what you want with the string
    displayLabel.attributedText = attributedText
}

The end result:

Bold part of localised string Parameterised string bold Swift NSAttributedString iOS

This should be flexible enough to work with any number of placeholders that are present in strings and you do not need to keep track of different placeholders.

NSAttributed string bold part of localised string with parameters format swift iOS

Shawn Frank
  • 4,381
  • 2
  • 19
  • 29
  • 1
    There should be a simple solution: Render the localized string with the placeholders into NSAttributedString, add the bold effects to `%@`, enumerate the bold attributes, and replace the text values found. That would avoid keeping track of indices. Also, if you want to get rid of `offset` calculation, a little trick is to do it backwards, with `reversed()` – Larme Feb 22 '22 at 07:59
  • @Larme - I am open to improving / optimizing / simplifying my solution. However, I didn't fully grasp your suggestions. `add the bold effects to %@` - how would i do this without REGEX to find out where they are ? `enumerate the bold attributes, and replace the text values found` - how, using https://developer.apple.com/documentation/foundation/nsattributedstring/1412070-enumerateattributes ? `a little trick is to do it backwards, with reversed()` - which part ? – Shawn Frank Feb 22 '22 at 08:19
  • I posted a solution, and I used to `reversed()` to illustrate. You still need the regex part. – Larme Feb 22 '22 at 08:33
  • @meaning-matters - could you give an example input of a scenario which would not work ? – Shawn Frank Feb 22 '22 at 08:57
  • @ShawnFrank "my name is %2$@ and i study in class %1$@" for instance, saying that the first element in the values should be at `%1$@`, and the second `%2$@`. Imagine "blue car", in English, adjectives are always before the noun, but that's not the case for all languages... – Larme Feb 22 '22 at 09:00
  • I see your point @Larme and yes, my solution is a more `naive` implementation which is how Strings `init(format:arguments:)` work which replaces the parameters in order as that is what I thought the OP was after. However yes, my solution has a limitation based on your use case. – Shawn Frank Feb 22 '22 at 09:13
0
let descriptionString = String(format: "localised_key".localized(), Harsh, 10)
let description = NSMutableAttributedString(string: descriptionString, attributes: [NSAttributedString.Key.font: UIFont(name: "NotoSans-Regular", size: 15.7)!, NSAttributedString.Key.foregroundColor: UIColor(rgb: 0x000b38), NSAttributedString.Key.kern: 0.5])
let rangeName = descriptionString.range(of: "Harsh")
let rangeClass = descriptionString.range(of: "10")
let nsrangeName = NSRange(rangeName!, in: descriptionString)
let nsrangeClass = NSRange(rangeClass!, in: descriptionString)
description.addAttributes([NSAttributedString.Key.font: UIFont(name: "NotoSans-Bold", size: 15.7)!, NSAttributedString.Key.foregroundColor: UIColor(rgb: 0x000b38), NSAttributedString.Key.kern: 0.5], range: nsrangeName)
description.addAttributes([NSAttributedString.Key.font: UIFont(name: "NotoSans-Bold", size: 15.7)!, NSAttributedString.Key.foregroundColor: UIColor(rgb: 0x000b38), NSAttributedString.Key.kern: 0.5], range: nsrangeClass)

for more references, use this

Wimukthi Rajapaksha
  • 961
  • 1
  • 11
  • 23