Here is the working code:
#import <Foundation/Foundation.h> @interface NSAttributedString (AttributedFormat) - (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ...; - (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments; @end @implementation NSAttributedString (AttributedFormat) - (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ... { va_list args; va_start(args, attrFormat); self = [self initWithFormat:attrFormat arguments:args]; va_end(args); return self; } - (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments { NSRegularExpression *regex; regex = [[NSRegularExpression alloc] initWithPattern: @"(%.*?[@%dDuUxXoOfeEgGccsSpaAF])" options: 0 error: nil]; NSString *format = attrFormat.string; format = [regex stringByReplacingMatchesInString: format options: 0 range: NSMakeRange(0, format.length) withTemplate: @"\0$1\0"]; NSString *result = [[NSString alloc] initWithFormat:format arguments:arguments]; NSMutableArray *f_comps = [format componentsSeparatedByString:@"\0"].mutableCopy; NSMutableArray *r_comps = [result componentsSeparatedByString:@"\0"].mutableCopy; NSMutableAttributedString *output = [[NSMutableAttributedString alloc] init]; __block int consumed_length = 0; [attrFormat enumerateAttributesInRange:NSMakeRange(0, attrFormat.length) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { NSMutableString *substr = [NSMutableString string]; while(f_comps.count > 0 && NSMaxRange(range) >= consumed_length + [(NSString *)f_comps[0] length]){ NSString *f_str = f_comps[0]; NSString *r_str = r_comps[0]; [substr appendString:r_str]; [f_comps removeObjectAtIndex:0]; [r_comps removeObjectAtIndex:0]; consumed_length += f_str.length; } NSUInteger idx = NSMaxRange(range) - consumed_length; if(f_comps.count > 0 && idx > 0) { NSString *f_str = f_comps[0]; NSString *leading = [f_str substringToIndex:idx]; [substr appendString:leading]; NSString *trailing = [f_str substringFromIndex:idx]; [f_comps replaceObjectAtIndex:0 withObject:trailing]; [r_comps replaceObjectAtIndex:0 withObject:trailing]; consumed_length += idx; } [output appendAttributedString:[[NSAttributedString alloc] initWithString:substr attributes:attrs]]; }]; return [self initWithAttributedString:output]; } @end
Usage example:
NSMutableAttributedString *fmt = [[NSMutableAttributedString alloc] initWithString:@"test: "]; [fmt appendAttributedString: [[NSAttributedString alloc] initWithString: @"Some%%string" attributes: @{ NSFontAttributeName: [UIFont systemFontOfSize:17] }]]; [fmt appendAttributedString: [[NSAttributedString alloc] initWithString: @"bold %@ template %.3f %d" attributes: @{ NSFontAttributeName: [UIFont boldSystemFontOfSize:20], NSForegroundColorAttributeName: [UIColor redColor] }]]; [fmt appendAttributedString: [[NSAttributedString alloc] initWithString: @"%@ blah blah blah" attributes: @{ NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor blueColor] }]]; NSAttributedString *result = [[NSAttributedString alloc] initWithFormat:fmt, @"[foo]", 1.23, 56, @"[[bar]]"];
Result:

It may still have some errors, but it should work in most cases.
(%.*?[@%dDuUxXoOfeEgGccsSpaAF])
This regular expression matches "Format Specifiers . " the qualifier begins with %
and ends with the specified characters. and may have some modifiers in between. This is not ideal, for example, this illegal format "%__s"
should be ignored, but my regular expression matches this entire line. but as long as the specifier is legal, it should work.
My code matches it and inserts delimiters around qualifiers:
I'm %s. I'm <delimiter>%s<delimiter>.
I use \0
as a delimiter.
I'm \0%s\0.
then interpolate it.
I'm \0rintaro\0.
then split the format and result into a separator:
f_comps: ["I'm ", "%s", "."] r_comps: ["I'm ", "rintaro", "."]
Here, the total length of the f_comps
string f_comps
exactly the same as the original attribute format. Then, iterating over the attributes using enumerateAttributesInRange
, we can apply the attributes to the results.
Sorry, but itβs too difficult to explain the work inside enumerateAttributesInRange
, with my weak English skills :)