68

I want to replace a substring (e.g. @"replace") of an NSAttributedString with another NSAttributedString.

I am looking for an equivalent method to NSString's stringByReplacingOccurrencesOfString:withString: for NSAttributedString.

Demitri
  • 13,134
  • 4
  • 40
  • 41
Garoal
  • 2,364
  • 2
  • 19
  • 31

10 Answers10

83
  1. Convert your attributed string into an instance of NSMutableAttributedString.

  2. The mutable attributed string has a mutableString property. According to the documentation:

    "The receiver tracks changes to this string and keeps its attribute mappings up to date."

    So you can use the resulting mutable string to execute the replacement with replaceOccurrencesOfString:withString:options:range:.

Ole Begemann
  • 135,006
  • 31
  • 278
  • 256
  • Isn't this private API as the mutable string property is read only? – Michal Shatz Dec 24 '14 at 14:46
  • @MichalShatz No. The property being read-only only means that you cannot assign a different object to it (i.e., you can’t call `setMutableString:`). But it is perfectly fine to modify the mutable string object in place. That’s the whole reason this property exists. – Ole Begemann Dec 27 '14 at 10:32
  • 1
    Is this really an answer? I am getting a casting problem with the second parameter (withString) being an attributedString – Knight0fDragon Aug 31 '15 at 21:06
  • 17
    The method "replaceOccurrencesOfString:withString:options:range:" is applied for NSMutableString, not for NSAttributedString or NSMutableAttributedString. If you convert to NSMutableString make the replacements there, you will lose its attributes after the conversion. The question is about how to get NSAttributedString as the result, not just string, so the answer is totally irrelevant but it got so many upvotes. – Darius Miliauskas Mar 25 '16 at 08:18
  • Darius Miliauskas Do you know any workaround for it ? – shinoys222 Mar 28 '16 at 20:51
  • 6
    This works fine, example replacing new line chars: `[[result mutableString] replaceOccurrencesOfString:@"\n" withString:@" " options:NSCaseInsensitiveSearch range:NSMakeRange(0, result.length)];` – mofojed Dec 29 '16 at 15:01
21

Here is how you can change the string of NSMutableAttributedString, while preserving its attributes:

Swift:

// first we create a mutable copy of attributed text 
let originalAttributedText = nameLabel.attributedText?.mutableCopy() as! NSMutableAttributedString

// then we replace text so easily
let newAttributedText = originalAttributedText.mutableString.setString("new text to replace")

Objective-C:

NSMutableAttributedString *newAttrStr = [attribtedTxt.mutableString setString:@"new string"];
Hashem Aboonajmi
  • 13,077
  • 8
  • 66
  • 75
  • Your example will probably not even compile. A C-string stored in an NSMutableAttributedString? A replacement with no effect because you did it on a copy (`mutableString`) without reference? – Cœur Aug 17 '15 at 16:08
  • This does not work. ```let str = NSMutableAttributedString(string: "HERE") str.addAttribute(.foregroundColor, value: NSColor.blue, range: NSMakeRange(1, 1)); print(str); str.mutableString.setString("THERE"); print(str)```. Running this code prints: `H{ }E{ NSColor = "sRGB IEC61966-2.1 colorspace 0 0 1 1"; }RE{ }` the first time, and `THERE{ }` the second time. Attributes gone. – Peter R Jul 04 '22 at 14:00
20

In my case, the following way was the only (tested on iOS9):

NSAttributedString *attributedString = ...;
NSAttributedString *anotherAttributedString = ...; //the string which will replace

while ([attributedString.mutableString containsString:@"replace"]) {
        NSRange range = [attributedString.mutableString rangeOfString:@"replace"];
        [attributedString replaceCharactersInRange:range  withAttributedString:anotherAttributedString];
    }

Of course it will be nice to find another better way.

Darius Miliauskas
  • 3,391
  • 4
  • 35
  • 53
  • 2
    This is more appropriate for replacing a string at a given range, rather than an occurrence, as strings may contain duplicate substrings. – Will Von Ullrich Apr 13 '17 at 16:32
19

Swift 4: Updated sunkas excellent solution to Swift 4 and wrapped in "extension". Just clip this into your ViewController (outside the class) and use it.

extension NSAttributedString {
    func stringWithString(stringToReplace: String, replacedWithString newStringPart: String) -> NSMutableAttributedString
    {
        let mutableAttributedString = mutableCopy() as! NSMutableAttributedString
        let mutableString = mutableAttributedString.mutableString
        while mutableString.contains(stringToReplace) {
            let rangeOfStringToBeReplaced = mutableString.range(of: stringToReplace)
            mutableAttributedString.replaceCharacters(in: rangeOfStringToBeReplaced, with: newStringPart)
        }
        return mutableAttributedString
    }
}
gundrabur
  • 843
  • 10
  • 12
  • "Just clip this into your ViewController (outside the class) and use it." - So that is the best place to put your extensions right? – StackUnderflow Dec 14 '19 at 14:41
  • I didn't say that, I just said it will work that way and it does. Where do you put all your extensions in? – gundrabur Dec 14 '19 at 18:16
  • 3
    Well you have to be careful what you say, there are a lot of developers learning bad habits in here. If you have extensions for NSAttributedString then create a file called "NSAttributedString+Extensions.swift" and put it in a folder called "Extensions". – StackUnderflow Dec 15 '19 at 00:36
17

With Swift 4 and iOS 11, you can use one of the 2 following ways in order to solve your problem.


#1. Using NSMutableAttributedString replaceCharacters(in:with:) method

NSMutableAttributedString has a method called replaceCharacters(in:with:). replaceCharacters(in:with:) has the following declaration:

Replaces the characters and attributes in a given range with the characters and attributes of the given attributed string.

func replaceCharacters(in range: NSRange, with attrString: NSAttributedString)

The Playground code below shows how to use replaceCharacters(in:with:) in order to replace a substring of an NSMutableAttributedString instance with a new NSMutableAttributedString instance:

import UIKit

// Set initial attributed string
let initialString = "This is the initial string"
let attributes = [NSAttributedStringKey.foregroundColor : UIColor.red]
let mutableAttributedString = NSMutableAttributedString(string: initialString, attributes: attributes)

// Set new attributed string
let newString = "new"
let newAttributes = [NSAttributedStringKey.underlineStyle : NSUnderlineStyle.styleSingle.rawValue]
let newAttributedString = NSMutableAttributedString(string: newString, attributes: newAttributes)

// Get range of text to replace
guard let range = mutableAttributedString.string.range(of: "initial") else { exit(0) }
let nsRange = NSRange(range, in: mutableAttributedString.string)

// Replace content in range with the new content
mutableAttributedString.replaceCharacters(in: nsRange, with: newAttributedString)

#2. Using NSMutableString replaceOccurrences(of:with:options:range:) method

NSMutableString has a method called replaceOccurrences(of:with:options:range:). replaceOccurrences(of:with:options:range:) has the following declaration:

Replaces all occurrences of a given string in a given range with another given string, returning the number of replacements.

func replaceOccurrences(of target: String, with replacement: String, options: NSString.CompareOptions = [], range searchRange: NSRange) -> Int

The Playground code below shows how to use replaceOccurrences(of:with:options:range:) in order to replace a substring of an NSMutableAttributedString instance with a new NSMutableAttributedString instance:

import UIKit

// Set initial attributed string
let initialString = "This is the initial string"
let attributes = [NSAttributedStringKey.foregroundColor : UIColor.red]
let mutableAttributedString = NSMutableAttributedString(string: initialString, attributes: attributes)

// Set new string
let newString = "new"

// Replace replaceable content in mutableAttributedString with new content
let totalRange = NSRange(location: 0, length: mutableAttributedString.string.count)
_ = mutableAttributedString.mutableString.replaceOccurrences(of: "initial", with: newString, options: [], range: totalRange)

// Get range of text that requires new attributes
guard let range = mutableAttributedString.string.range(of: newString) else { exit(0) }
let nsRange = NSRange(range, in: mutableAttributedString.string)

// Apply new attributes to the text matching the range
let newAttributes = [NSAttributedStringKey.underlineStyle : NSUnderlineStyle.styleSingle.rawValue]
mutableAttributedString.setAttributes(newAttributes, range: nsRange)
Imanou Petit
  • 89,880
  • 29
  • 256
  • 218
6

I had to bold text in <b> tags, here what I've done:

- (NSAttributedString *)boldString:(NSString *)string {
    UIFont *boldFont = [UIFont boldSystemFontOfSize:14];
    NSMutableAttributedString *attributedDescription = [[NSMutableAttributedString alloc] initWithString:string];

    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@".*?<b>(.*?)<\\/b>.*?" options:NSRegularExpressionCaseInsensitive error:NULL];
    NSArray *myArray = [regex matchesInString:string options:0 range:NSMakeRange(0, string.length)] ;
    for (NSTextCheckingResult *match in myArray) {
        NSRange matchRange = [match rangeAtIndex:1];
        [attributedDescription addAttribute:NSFontAttributeName value:boldFont range:matchRange];
    }
    while ([attributedDescription.string containsString:@"<b>"] || [attributedDescription.string containsString:@"</b>"]) {
        NSRange rangeOfTag = [attributedDescription.string rangeOfString:@"<b>"];
        [attributedDescription replaceCharactersInRange:rangeOfTag withString:@""];
        rangeOfTag = [attributedDescription.string rangeOfString:@"</b>"];
        [attributedDescription replaceCharactersInRange:rangeOfTag withString:@""];
    }
    return attributedDescription;
}
trickster77777
  • 1,198
  • 2
  • 19
  • 30
  • 1
    Thanks for this, trickster! I've been trying so hard to implement such a function to my project, but with different tag (e.g. ). After some hours of work, I decided to try this out but with some few modifications, like having a custom font. Thanks! – Glenn Posadas Jul 10 '18 at 09:42
4
NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:@"I am a boy."];
[result addAttribute:NSForegroundColorAttributeName value:[UIColor blackColor] range:NSMakeRange(0, [result length])];

NSMutableAttributedString *replace = [[NSMutableAttributedString alloc] initWithString:@"a"];
[replace addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:NSMakeRange(0, [replace length])];

[result replaceCharactersInRange:NSMakeRange(5, [replace length]) withAttributedString:replace];
4

I find that all of the other answers does not work. Here is how I replaced content of a NSAttributed string in a category extension:

func stringWithString(stringToReplace:String, replacedWithString newStringPart:String) -> NSMutableAttributedString
{
    let mutableAttributedString = mutableCopy() as! NSMutableAttributedString
    let mutableString = mutableAttributedString.mutableString

    while mutableString.containsString(stringToReplace) {
        let rangeOfStringToBeReplaced = mutableString.rangeOfString(stringToReplace)
        mutableAttributedString.replaceCharactersInRange(rangeOfStringToBeReplaced, withString: newStringPart)
    }
    return mutableAttributedString
}
Sunkas
  • 9,542
  • 6
  • 62
  • 102
3

I have a specific requirement and fixed like below. This might help someone.

Requirement: In the storyboard, rich text directly added to UITextView's attribute which contains a word "App Version: 1.0". Now I have to dynamise the version number by reading it from info plist.

Solution: Deleted version number 1.0 from the storyboard, just kept "App Version:" and added below code.

NSAttributedString *attribute = self.firsttextView.attributedText;
NSMutableAttributedString *mutableAttri = [[NSMutableAttributedString alloc] initWithAttributedString:attribute];
NSString *appVersionText = @"App Version:";
if ([[mutableAttri mutableString] containsString:appVersionText]) {
    NSDictionary* infoDict = [[NSBundle mainBundle] infoDictionary];
    NSString* version = [infoDict objectForKey:@"CFBundleShortVersionString"];
    NSString *newappversion = [NSString stringWithFormat:@"%@ %@",appVersionText,version] ;
    [[mutableAttri mutableString] replaceOccurrencesOfString:appVersionText withString:newappversion options:NSCaseInsensitiveSearch range:NSMakeRange(0, mutableAttri.length)];
    self.firsttextView.attributedText = mutableAttri;
}

Done!! Updated/modified attributedText.

2

i created a Swift 5 extension for that

extension NSMutableAttributedString {
    
    func replace(_ findString: String, with replacement: String, attributes: [NSAttributedString.Key : Any]) {
        
        let ms = mutableString
        
        var range = ms.range(of: findString)
        while range.location != NSNotFound {
            addAttributes(attributes, range: range)
            ms.replaceCharacters(in: range, with: replacement)
            
            range = ms.range(of: findString)
        }
        
    }

}

use case

attributedString.replace("%EMAIL%", with: email, attributes: [.font:boldFont])
        
Peter Lapisu
  • 19,915
  • 16
  • 123
  • 179