0

I'm using attributed text in a UITextView. The text contain many fonts.

I'm looking for a way to replace a particular font by another one.

Any swift approach for this? :)

Franck
  • 8,939
  • 8
  • 39
  • 57

2 Answers2

1

There is a method called attributesAtIndex(_:effectiveRange:). This method returns a dictionary that you can get the font from.

You will need to iterate over each index to store the fonts. It will be slow, because in text files the font could potentially change every character. So I would recommend doing this off the main thread.

TigerCoding
  • 8,710
  • 10
  • 47
  • 72
  • Hi. Ouch. Hope there is a faster way. Anyway, look like this is a way. :) – Franck Jul 21 '15 at 19:34
  • I doubt there is a faster way, as you have to inspect each character index. Hope this helps. – TigerCoding Jul 21 '15 at 19:53
  • Yes and no. Still look for a faster way. Anyway i'll check it as a solution. Thanks again. – Franck Jul 22 '15 at 19:02
  • You can also enumerate attributes looking for only the `NSFontAttributeName`. There is a method in `NSAttributedString` for that, and check the Font value (`fontName` I guess property of `UIFont`) and replace it if needed). – Larme Jul 23 '15 at 06:13
  • Not sure how much more optimized this would be, but certainly much easier. @Larme maybe you should post this as a better answer. – TigerCoding Jul 23 '15 at 20:07
  • @TigerCoding: I posted my solution, compare also with yours (from what I understood), I don't know which one is really faster. Mine seems just more understandable (since the `enumerateAttribute:ZzZ` is more explicit), but not really less code. – Larme Jul 25 '15 at 14:56
1

My code will be in Objective-C, but since we use both CocoaTouch, it should be the same logic.

The method I use is enumerateAttribute:inRange:options:usingBlock: to look only for NSFontAttributeName.

There is another point that isn't discussed: How recognize that the font is the one searched. Are you looking for familyName, fontName (property of UIFont? Even in the same family, font may look a lot different and you may want to search really for the one exactly matching the same name. I've discussed once about Font Names here. You may found it interesting in your case. Note that there are methods (that I didn't know at the time) that can get the Bold Name of the font if available (or italic, etc.)

The main code in Objective-C is this one:

[attrString enumerateAttribute:NSFontAttributeName
                           inRange:NSMakeRange(0, [attrString length])
                           options:0
                        usingBlock:^(id value, NSRange range, BOOL *stop) {
                            UIFont *currentFont = (UIFont *)value; //Font currently applied in this range
                            if ([self isFont:currentFont sameAs:searchedFont]) //This is where it can be tricky
                            {
                            [attrString addAttribute:NSFontAttributeName
                                               value:replacementFont
                                               range:range];
                            }
    }];

Possible change/adaptation according to your needs: Change the font, but not the size:

[attrString addAttribute:NSFontAttributeName value:[UIFont fontWithName:replaceFontName size:currentFont.pointSize]; range:range];

Sample test code:

UIFont *replacementFont = [UIFont boldSystemFontOfSize:12];
UIFont *searchedFont    = [UIFont fontWithName:@"Helvetica Neue" size:15];
UIFont *normalFont      = [UIFont italicSystemFontOfSize:14];

NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet,"];

NSAttributedString *attrStr1 = [[NSAttributedString alloc] initWithString:@"consectetuer adipiscing elit." attributes:@{NSFontAttributeName:searchedFont, NSForegroundColorAttributeName:[UIColor redColor]}];

NSAttributedString *attrStr2 = [[NSAttributedString alloc] initWithString:@" Aenean commodo ligula eget dolor." attributes:@{NSFontAttributeName:normalFont}];

NSAttributedString *attrStr3 = [[NSAttributedString alloc] initWithString:@" Aenean massa." attributes:@{NSFontAttributeName:searchedFont}];

NSAttributedString *attrStr4 = [[NSAttributedString alloc] initWithString:@"Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim."];

[attrString appendAttributedString:attrStr1];
[attrString appendAttributedString:attrStr2];
[attrString appendAttributedString:attrStr3];
[attrString appendAttributedString:attrStr4];

NSLog(@"AttrString: %@", attrString);

[attrString enumerateAttribute:NSFontAttributeName
                       inRange:NSMakeRange(0, [attrString length])
                       options:0
                    usingBlock:^(id value, NSRange range, BOOL *stop) {
                        UIFont *currentFont = (UIFont *)value;
                        if ([self isFont:currentFont sameAs:searchedFont])
                        {
            [attrString addAttribute:NSFontAttributeName
                               value:replacementFont
                               range:rangeEffect];
                        }
}];

NSLog(@"AttrString Changed: %@", attrString);

With the solution of @TigerCoding, here is the possible code:

NSInteger location = 0;
while (location < [attrString length])
{
    NSRange rangeEffect;
    NSDictionary *attributes = [attrString attributesAtIndex:location effectiveRange:&rangeEffect];
    if (attributes[NSFontAttributeName])
    {
        UIFont *font = attributes[NSFontAttributeName];
        if ([self isFont:font sameAs:searchedFont])
        {
            [attrString addAttribute:NSFontAttributeName value:replacementFont range:rangeEffect];
        }
    }
    location+=rangeEffect.length;
}

As a side note: A few optimization to test (but will need some research). I think from a few example that if you apply the same attributeS for two consecutive range, NSAttributedString will "appends them" into one, in case you may be afraid to apply the same effect consecutively. So the question is that if you have @{NSFontAttributeName:font1, NSForegroundColorAttributeName:color1} for range 0,3 and @{NSFontAttributeName:font1, NSForegroundColorAttributeName:color2} for range 3,5 Will enumerateAttribute:inRange:options:usingBlock: return you the range 0,5? Will it be faster than enumerating each indexes?

Community
  • 1
  • 1
Larme
  • 24,190
  • 6
  • 51
  • 81