Are there any analogues of [NSString stringWithFormat:] for NSAttributedString
Asked Answered
P

5

18

Usually I build app interface in interface builder. Sometimes design requires to use attributed strings (fonts, colors and etc.). It's easy to configure if string is static.
But if string is dynamic (format with arguments) then there are no ways to configure attributes in interface builder. It requires to write a lot of code.
I am looking for some analogues of [NSString stringWithFormat:] for NSAttributedString. So I will be able to set string format and necessary attributes in interface builder, and then provide necessary arguments in code.

For example:
Let's consider that I need display string with such format: "%d + %d = %d" (all numbers are bold).
I want to configure this format in interface builder. In code I want to provide arguments: 1, 1, 2. App should show "1 + 1 = 2".

Photomap answered 20/7, 2015 at 19:33 Comment(0)
P
4

I was looking for good existing solution for this question, but no success.
So I was able to implement it on my own.
That's why I am self-answering the question to share the knowledge with community.

Solution

NSAttributedString+VPAttributedFormat category provides methods for building attributed string based on attributed format and arguments that should satisfy this format.
The most suitable case of using this category is text controls with variable attributed text configured in interface builder.
You need set correct string format to attributed text and configure necessary attributes.
Then you need pass necessary arguments in code by using methods of this category.

  • Format syntax is the same as in [NSString stringWithFormat:] method;
  • Can be used in Objective C and Swift code;
  • Requires iOS 6.0 and later;
  • Integrated with CocoaPods;
  • Covered with unit tests.

Usage

1. Import framework header or module

// Objective C
// By header
#import <VPAttributedFormat/VPAttributedFormat.h>

// By module
@import VPAttributedFormat;

// Swift
import VPAttributedFormat

2. Set correct format and attributes for text control in interface builder
usage

3. Create IBOutlet and link it with text control

// Objective C
@property (nonatomic, weak) IBOutlet UILabel *textLabel;

// Swift
@IBOutlet weak var textLabel: UILabel!

4. Populate format with necessary arguments

// Objective C
NSString *hot = @"Hot";
NSString *cold = @"Cold";
  
self.textLabel.attributedText = [NSAttributedString vp_attributedStringWithAttributedFormat:self.textLabel.attributedText,
                                 hot,
                                 cold];

// Swift
let hot = "Hot"
let cold = "Cold"
var arguments: [CVarArgType] = [hot, cold]
textLabel.attributedText = withVaList(arguments) { pointer in
    NSAttributedString.vp_attributedStringWithAttributedFormat(textLabel.attributedText, arguments: pointer)
}

5. See result
result

Examples

VPAttributedFormatExample is an example project. It provides Basic and Pro format examples.
example

Photomap answered 20/7, 2015 at 19:33 Comment(1)
Very nice, I was just needing this!!Jadeite
B
15

Compatible with Swift 4.2

public extension NSAttributedString {
    convenience init(format: NSAttributedString, args: NSAttributedString...) {
        let mutableNSAttributedString = NSMutableAttributedString(attributedString: format)

        args.forEach { (attributedString) in
            let range = NSString(string: mutableNSAttributedString.string).range(of: "%@")
            mutableNSAttributedString.replaceCharacters(in: range, with: attributedString)
        }
        self.init(attributedString: mutableNSAttributedString)
    }
}

Usage:

let content = NSAttributedString(string: "The quick brown %@ jumps over the lazy %@")
let fox = NSAttributedString(string: "fox", attributes: [.font: Fonts.CalibreReact.boldItalic.font(size: 40)])
let dog = NSAttributedString(string: "dog", attributes: [.font: Fonts.CalibreReact.lightItalic.font(size: 11)])
attributedLabel.attributedText = NSAttributedString(format: content, args: fox, dog)

Result:

enter image description here

Bluepoint answered 12/6, 2019 at 15:7 Comment(6)
Just a little reminder that this should not be used for unsanitized arguments (e.g. if fox == "%@" the result will be The quick brown dog jups over the lazy %@).Fastigium
results -> Terminating app due to uncaught exception 'NSRangeException', reason: 'NSMutableRLEArray insertObject:range:: Out of bounds'Reefer
@UtkuDalmaz you might not have matched the number of arguments provided with the format string. I could improve my answer by adding a fatalError line with a more specific message.Bluepoint
Could also be made safer by adding if range.location != NSNotFound {Eiderdown
@Eiderdown is this working fine after adding if range.location != NSNotFound ?Slop
@Nitkarsh Gupta I'm sure it would be, give it a go :)Eiderdown
P
4

I was looking for good existing solution for this question, but no success.
So I was able to implement it on my own.
That's why I am self-answering the question to share the knowledge with community.

Solution

NSAttributedString+VPAttributedFormat category provides methods for building attributed string based on attributed format and arguments that should satisfy this format.
The most suitable case of using this category is text controls with variable attributed text configured in interface builder.
You need set correct string format to attributed text and configure necessary attributes.
Then you need pass necessary arguments in code by using methods of this category.

  • Format syntax is the same as in [NSString stringWithFormat:] method;
  • Can be used in Objective C and Swift code;
  • Requires iOS 6.0 and later;
  • Integrated with CocoaPods;
  • Covered with unit tests.

Usage

1. Import framework header or module

// Objective C
// By header
#import <VPAttributedFormat/VPAttributedFormat.h>

// By module
@import VPAttributedFormat;

// Swift
import VPAttributedFormat

2. Set correct format and attributes for text control in interface builder
usage

3. Create IBOutlet and link it with text control

// Objective C
@property (nonatomic, weak) IBOutlet UILabel *textLabel;

// Swift
@IBOutlet weak var textLabel: UILabel!

4. Populate format with necessary arguments

// Objective C
NSString *hot = @"Hot";
NSString *cold = @"Cold";
  
self.textLabel.attributedText = [NSAttributedString vp_attributedStringWithAttributedFormat:self.textLabel.attributedText,
                                 hot,
                                 cold];

// Swift
let hot = "Hot"
let cold = "Cold"
var arguments: [CVarArgType] = [hot, cold]
textLabel.attributedText = withVaList(arguments) { pointer in
    NSAttributedString.vp_attributedStringWithAttributedFormat(textLabel.attributedText, arguments: pointer)
}

5. See result
result

Examples

VPAttributedFormatExample is an example project. It provides Basic and Pro format examples.
example

Photomap answered 20/7, 2015 at 19:33 Comment(1)
Very nice, I was just needing this!!Jadeite
C
4

Here's a category I wrote to add the method to NSAttributedString. You'll have to pass in NULL as the last argument to the function however, otherwise it will crash to the va_list restrictions on detecting size. [attributedString stringWithFormat:attrFormat, attrArg1, attrArg2, NULL];

@implementation NSAttributedString(stringWithFormat)

+(NSAttributedString*)stringWithFormat:(NSAttributedString*)format, ...{
    va_list args;
    va_start(args, format);

    NSMutableAttributedString *mutableAttributedString = (NSMutableAttributedString*)[format mutableCopy];
    NSString *mutableString = [mutableAttributedString string];

    while (true) {
        NSAttributedString *arg = va_arg(args, NSAttributedString*);
        if (!arg) {
            break;
        }
        NSRange rangeOfStringToBeReplaced = [mutableString rangeOfString:@"%@"];
        [mutableAttributedString replaceCharactersInRange:rangeOfStringToBeReplaced withAttributedString:arg];
    }

    va_end(args);

    return mutableAttributedString;
}
@end
Coltun answered 26/8, 2016 at 19:32 Comment(1)
This will only work if you keep changing mutableString inside the loop as well. Otherwise all parameters will go into the first placeholder and at the end we have the last parameter on place of the first placeholder.Northcutt
A
3

Here is a Swift 4 extension based on TheJeff's answer (corrected for multiple substitutions). It is restricted to substituting placeholders with NSAttributedString's:

public extension NSAttributedString {
    convenience init(format: NSAttributedString, args: NSAttributedString...) {
        let mutableNSAttributedString = NSMutableAttributedString(attributedString: format)

        var nsRange = NSString(string: mutableNSAttributedString.string).range(of: "%@")
        var param = 0
        while nsRange.location != NSNotFound {
            guard args.count > 0, param < args.count else {
                fatalError("Not enough arguments provided for \(format)")
            }

            mutableNSAttributedString.replaceCharacters(in: nsRange, with: args[param])
            param += 1
            nsRange = NSString(string: mutableNSAttributedString.string).range(of: "%@")
        }

        self.init(attributedString: mutableNSAttributedString)
    }
}
Atthia answered 6/3, 2019 at 20:34 Comment(2)
Can you improve this without fatalError?Triplicity
Well, it is a programming bug if you didn’t match the arguments provided with the format string. I consider programming errors fatal and should never ship. If you want you could assert instead and just return the original string but I leave that up to the reader to decide how they want to program.Atthia
L
1

Some of the other answers assume that there is a fixed order of the arguments, which is not necessarily the case when using localized strings returned by NSLocalizedString(). The following code takes this into account. It assumes that the format string contains only %@ format specifiers (e.g. when there is a single argument, or a non-localized string), or a sequence of %1$@, %2$@, %3$@, etc., which are allowed to change their position within the format string and are still mapped to the correct argument:

extension NSMutableAttributedString {
    
    private static let formatRegex = try! NSRegularExpression(pattern: "%(?:(\\d+)\\$)?@")
    
    convenience init(format: String, arguments: [NSAttributedString]) {
        self.init(string: format)
        var i = 0
        var location = 0
        while let match = NSMutableAttributedString.formatRegex.firstMatch(in: string, range: NSRange(location: location, length: length - location)) {
            let index = match.range(at: 1).location == NSNotFound ? i : Int((string as NSString).substring(with: match.range(at: 1)))! - 1
            let argument = arguments[index]
            addAttributes(argument.attributes(at: 0, effectiveRange: nil), range: match.range)
            replaceCharacters(in: match.range, with: argument.string)
            i += 1
            location = match.range.location + argument.length
        }
    }
    
}

Usage:

let string = NSMutableAttributedString(format: "From %1$@ to %2$@.", arguments: [
    NSAttributedString(string: "left", attributes: [.foregroundColor: NSColor.red]),
    NSAttributedString(string: "right", attributes: [.foregroundColor: NSColor.blue])
])

You could make it accept variadic arguments instead of an array if you prefer:

convenience init(format: String, arguments: NSAttributedString...) {
    ...
}
Lemcke answered 7/3, 2023 at 17:4 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.