71

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.

1
  • For replacing text in a given range (not each occurrence) see the answer @Darius gave below Commented Apr 13, 2017 at 16:33

11 Answers 11

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:.

Sign up to request clarification or add additional context in comments.

6 Comments

Isn't this private API as the mutable string property is read only?
@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.
Is this really an answer? I am getting a casting problem with the second parameter (withString) being an attributedString
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.
This works fine, example replacing new line chars: [[result mutableString] replaceOccurrencesOfString:@"\n" withString:@" " options:NSCaseInsensitiveSearch range:NSMakeRange(0, result.length)];
|
21

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.

1 Comment

This is more appropriate for replacing a string at a given range, rather than an occurrence, as strings may contain duplicate substrings.
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"];

2 Comments

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?
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.
19

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)

Comments

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
    }
}

3 Comments

"Just clip this into your ViewController (outside the class) and use it." - So that is the best place to put your extensions right?
I didn't say that, I just said it will work that way and it does. Where do you put all your extensions in?
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".
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;
}

1 Comment

Thanks for this, trickster! I've been trying so hard to implement such a function to my project, but with different tag (e.g. <i>). After some hours of work, I decided to try this out but with some few modifications, like having a custom font. Thanks!
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];

Comments

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
}

Comments

4

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.

Comments

4

i created a Swift 5 extension for that

extension NSAttributedString {
    
    func replacingOccurrences(of target: String, with replacement: String, attributes: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString {
        
        let s = NSMutableAttributedString(attributedString: self)
        s.beginEditing()
        s.replaceOccurrences(of: target, with: replacement, attributes: attributes)
        s.endEditing()
        return s
        
    }
    
}

extension NSMutableAttributedString {

    func replaceOccurrences(of target: String, with replacement: String, attributes: [NSAttributedString.Key : Any]? = nil) {
        
        var searchRange = NSRange(location: 0, length: self.length)
        
        while let range = self.string.range(of: target, options: [], range: Range(searchRange, in: self.string)) {
            let nsRange = NSRange(range, in: self.string)
            self.replaceCharacters(in: nsRange, with: replacement)
            
            let newRange = NSRange(location: nsRange.location, length: replacement.count)
            if let attributes = attributes {
                self.addAttributes(attributes, range: newRange)
            }
            
            searchRange = NSRange(location: newRange.upperBound, length: self.length - newRange.upperBound)
        }
        
    }
    
}

use case

attributedString.replacingOccurrences(of: "%EMAIL%", with: email, attributes: [.font:boldFont])
        

Comments

0

the full solution

extension NSAttributedString {
    
    func replacingOccurrences(of target: String, with replacement: String, attributes: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString {
        
        let s = NSMutableAttributedString(attributedString: self)
        s.beginEditing()
        s.replaceOccurrences(of: target, with: replacement, attributes: attributes)
        s.endEditing()
        return s
        
    }
    
}

extension NSMutableAttributedString {

    func replaceOccurrences(of target: String, with replacement: String, attributes: [NSAttributedString.Key : Any]? = nil) {
        
        var searchRange = NSRange(location: 0, length: self.length)
        
        while let range = self.string.range(of: target, options: [], range: Range(searchRange, in: self.string)) {
            let nsRange = NSRange(range, in: self.string)
            self.replaceCharacters(in: nsRange, with: replacement)
            
            let newRange = NSRange(location: nsRange.location, length: replacement.count)
            if let attributes = attributes {
                self.addAttributes(attributes, range: newRange)
            }
            
            searchRange = NSRange(location: newRange.upperBound, length: self.length - newRange.upperBound)
        }
        
    }
    
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.