Attributed Text, Replace a Specific Font by Another Using Swift

Attributed text, replace a specific font by another using swift

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?

replace entire text string in NSAttributedString without modifying other attributes

You can use NSMutableAttributedString and just update the string, the attributes won't change.
Example:

NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:@"my string" attributes:@{NSForegroundColorAttributeName: [UIColor blueColor], NSFontAttributeName: [UIFont systemFontOfSize:20]}];

//update the string
[mutableAttributedString.mutableString setString:@"my new string"];

NSMutableAttributedString with different fonts

I guess that your code work, but you think that it doesn't because font sizes look pretty much equally.

Here is what I see with your code

Sample Image

And that's what I see when I change size to 2 and 37

Sample Image

And that's your original sizes (7 and 17), but for both strings, I've set the same text.

Sample Image

Change only fontsize of NSAttributedString

If you only want to change the size of any given font found in the attributed string then you can do:

let newStr = someAttributedString.mutableCopy() as! NSMutableAttributedString
newStr.beginEditing()
newStr.enumerateAttribute(.font, in: NSRange(location: 0, length: newStr.string.utf16.count)) { (value, range, stop) in
if let oldFont = value as? UIFont {
let newFont = oldFont.withSize(20) // whatever size you need
newStr.addAttribute(.font, value: newFont, range: range)
}
}
newStr.endEditing()

print(newStr)

This will keep all other attributes in place.

If you want to replace all fonts in a given attributed string with a single font of a given size but keep all other attributes such as bold and italic, see:
NSAttributedString, change the font overall BUT keep all other attributes?

Swift- How to remove attributed string values and make it appear as normal string. (Bold, Italic, Underline, Strikethrough)

You need to enumerate the attributes on the NSAttributedString, and change or remove them.

With a little help from another question to know if a font is bold/italic

extension UIFont {
var isBold: Bool {
return fontDescriptor.symbolicTraits.contains(.traitBold)
}

var isItalic: Bool {
return fontDescriptor.symbolicTraits.contains(.traitItalic)
}
}

To simplify "your true logic":

extension NSAttributedString {

enum Effects {
case bold
case italic
case underline
case strikethrough
}

var fullRange: NSRange {
NSRange(location: 0, length: length)
}
}

This should do the trick:

func remove(effects: NSAttributedString.Effects, on attributedString: NSAttributedString, range: NSRange) -> NSAttributedString {

let mutableAttributedString = NSMutableAttributedString(attributedString: attributedString)

mutableAttributedString.enumerateAttributes(in: range, options: []) { attributes, subrange, pointee in
switch effects {
case .bold:
if let currentFont = attributes[.font] as? UIFont, currentFont.isBold {
let newFont = UIFont.systemFont(ofSize: currentFont.pointSize)
// Since we can only have one attribute at a range (and all subranges including in it)
// there is no need to remove then add, adding it will replace the previous one
mutableAttributedString.addAttribute(.font, value: newFont, range: subrange)
}
case .italic:
if let currentFont = attributes[.font] as? UIFont, currentFont.isItalic {
let newFont = UIFont.systemFont(ofSize: currentFont.pointSize)
// Since we can only have one attribute at a range (and all subranges including in it)
// there is no need to remove then add, adding it will replace the previous one
mutableAttributedString.addAttribute(.font, value: newFont, range: subrange)
}
case .strikethrough:
if attributes[.strikethroughStyle] != nil {
mutableAttributedString.removeAttribute(.strikethroughStyle, range: subrange)
}
if attributes[.strikethroughColor] != nil {
mutableAttributedString.removeAttribute(.strikethroughColor, range: subrange)
}
case .underline:
if attributes[.underlineColor] != nil {
mutableAttributedString.removeAttribute(.underlineColor, range: subrange)
}
if attributes[.underlineStyle] != nil {
mutableAttributedString.removeAttribute(.underlineStyle, range: subrange)
}
}
}
return mutableAttributedString
}

In Playground, you can test with:

let boldText = NSAttributedString(string: "Bold text", attributes: [.font: UIFont.boldSystemFont(ofSize: 25)])
let unboldText = remove(effects: .bold, on: boldText, range: boldText.fullRange)
let italicText = NSAttributedString(string: "Italic text", attributes: [.font: UIFont.italicSystemFont(ofSize: 25)])
let unItalicText = remove(effects: .italic, on: italicText, range: italicText.fullRange)
let underlineText = NSAttributedString(string: "Underline text", attributes: [.underlineStyle: 1])
let unUnderlineText = remove(effects: .underline, on: underlineText, range: underlineText.fullRange)
let strikethroughText = NSAttributedString(string: "Strikethrough text", attributes: [.strikethroughStyle: 2])
let unStrikethroughText = remove(effects: .strikethrough, on: strikethroughText, range: strikethroughText.fullRange)

let attributedStrings = [boldText, unboldText, italicText, unItalicText, underlineText, unUnderlineText, strikethroughText, unStrikethroughText]

let fullAttributedString = attributedStrings.reduce(into: NSMutableAttributedString()) {
$0.append(NSAttributedString(string: "\n"))
$0.append($1)
}

The following test is to ensure that if there are other effects on the `NSAttributedString`, they aren't removed.

let textView = UITextView(frame: CGRect(x: 0, y: 0, width: 500, height: 200))
textView.backgroundColor = .yellow
textView.attributedText = fullAttributedString
textView
textView.attributedText = remove(effects: .bold, on: fullAttributedString, range: fullAttributedString.fullRange)
textView
textView.attributedText = remove(effects: .italic, on: fullAttributedString, range: fullAttributedString.fullRange)
textView
textView.attributedText = remove(effects: .underline, on: fullAttributedString, range: fullAttributedString.fullRange)
textView
textView.attributedText = remove(effects: .strikethrough, on: fullAttributedString, range: fullAttributedString.fullRange)
textView

Nota bene:
You currently are doing bold OR italic, not both at the same time, since it's in this property is inside the UIFont, you'd need to find a bold AND italic font at the same time.

So if you want to remove an italic from a italic AND bold, you'd need to get the bold. I consider it as another scope, but there are questions about that.

Nota bene 2:

When you want to remove the attributes, I check for the presence of the attributes, but there is no need:

So:

if attributes[.someAttribute] != nil {
mutableAttributedString.removeAttribute(.someAttribute, range: subrange)
}

Could just be:

mutableAttributedString.removeAttribute(.someAttribute, range: subrange)

Nota bene 3:

func remove(effects: NSAttributedString.Effects, on attributedString: NSAttributedString, range: NSRange) -> NSAttributedString

Could be an extension on NSAttributedString/NSMutableAttributedString instead.



Related Topics



Leave a reply



Submit