41

I have a reference to NSAttributedString and i want to change the text of the attributed string.

I guess i have to created a new NSAttributedString and update the reference with this new string. However when i do this i lose the attributed of previous string.

NSAttributedString *newString = [[NSAttributedString alloc] initWithString:text];
[self setAttributedText:newString];

I have reference to old attributed string in self.attributedText. How can i retain the previous attributed in the new string?

KlimczakM
  • 12,576
  • 11
  • 64
  • 83
Chirag Jain
  • 2,378
  • 3
  • 21
  • 22

9 Answers9

41

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"];
Artal
  • 8,933
  • 2
  • 27
  • 30
39

Swift

Change the text while keeping the attributes:

let myString = "my string"
let myAttributes = [NSAttributedString.Key.foregroundColor: UIColor.blue, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 40)]
let mutableAttributedString = NSMutableAttributedString(string: myString, attributes: myAttributes)

let myNewString = "my new string"
mutableAttributedString.mutableString.setString(myNewString)

The results for mutableAttributedString are

  • enter image description here
  • enter image description here

Notes

Any sub-ranges of attributes beyond index 0 are discarded. For example, if I add another attribute to the last word of the original string, it is lost after I change the string:

// additional attribute added before changing the text
let myRange = NSRange(location: 3, length: 6)
let anotherAttribute = [ NSAttributedString.Key.backgroundColor: UIColor.yellow ]
mutableAttributedString.addAttributes(anotherAttribute, range: myRange)

Results:

  • enter image description here
  • enter image description here

From this we can see that the new string gets whatever the attributes are at index 0 of the original string. Indeed, if we adjust the range to be

let myRange = NSRange(location: 0, length: 1)

we get

  • enter image description here
  • enter image description here

See also

Suragch
  • 484,302
  • 314
  • 1,365
  • 1,393
7

I made a little extension to make this really easy:

import UIKit

extension UILabel {
    func setTextWhileKeepingAttributes(string: String) {
        if let newAttributedText = self.attributedText {
            let mutableAttributedText = newAttributedText.mutableCopy()

            mutableAttributedText.mutableString.setString(string)

            self.attributedText = mutableAttributedText as? NSAttributedString
        }
    }
}

https://gist.github.com/wvdk/e8992e82b04e626a862dbb991e4cbe9c

Wes
  • 71
  • 1
  • 3
  • 3
    I had to write like this: `(mutableAttributedText as AnyObject).mutableString.setString(string)` – Jonny Apr 06 '17 at 07:16
2

This is the way using Objective-C (tested on iOS 9)

NSAttributedString *primaryString = ...;
NSString *newString = ...;

//copy the attributes
NSDictionary *attributes = [primaryString attributesAtIndex:0 effectiveRange:NSMakeRange(primaryString.length-1, primaryString.length)];
NSMutableAttributedString *newString = [[NSMutableAttributedString alloc] initWithString:newString attributes:attributes];
NSMutableAttributedString *primaryStringMutable = [[NSMutableAttributedString alloc] initWithAttributedString:primaryString];

//change the string
[primaryStringMutable setAttributedString::newString];

primaryString = [NSAttributedString alloc] initWithAttributedString:primaryStringMutable];

Check for the most important references: attributesAtIndex:effectiveRange: and setAttributedString:.

Darius Miliauskas
  • 3,391
  • 4
  • 35
  • 53
  • For me, this fails to compile, with the error messaage `Sending 'NSRange' (aka 'struct _NSRange') to parameter of incompatible type 'NSRangePointer _Nullable' (aka 'struct _NSRange *')` in the line defining `attributes`. – phihag Jul 26 '20 at 21:29
2

Darius answer is almost there. It contains a minor error. The correct is:

This is the way using Objective-C (tested on iOS 10)

NSAttributedString *primaryString = ...;
NSString *newString = ...;

//copy the attributes
NSRange range = NSMakeRange(primaryString.length-1, primaryString.length);
NSDictionary *attributes = [primaryString attributesAtIndex:0 effectiveRange:&range];
NSMutableAttributedString *newString = [[NSMutableAttributedString alloc] initWithString:newString attributes:attributes];
NSMutableAttributedString *primaryStringMutable = [[NSMutableAttributedString alloc] initWithAttributedString:primaryString];

//change the string
[primaryStringMutable setAttributedString::newString];

primaryString = [NSAttributedString alloc] initWithAttributedString:primaryStringMutable];
Duck
  • 34,902
  • 47
  • 248
  • 470
  • This does not compile: newString is redeclared which is an error, the second-last line has two colons instead of one, and the last line is missing an opening [. Unfortunately attempts to edit the code to fix these errors have been rejected for "Changes are either completely superfluous or actively harm readability." Go figure. – ghr Dec 04 '18 at 20:54
2
        let mutableAttributedString  = mySubTitleLabel.attributedText?.mutableCopy() as? NSMutableAttributedString
        if let attrStr = mutableAttributedString{
            attrStr.mutableString.setString("Inner space can be an example shown on the, third page of the tutorial.")
            mySubTitleLabel.attributedText = attrStr;
        }

I hope this code may help you, i have copied the attribute of the label to a mutableAttributedString and then set the string for it

Syed Zahid Shah
  • 391
  • 3
  • 5
1

For those of you working with UIButtons, here is an improved answer based on Wes's.

It seemed that updating a label of a button had better be done this way:

let newtext = "my new text"
myuibutton.setAttributedTitle(titlelabel.getTextWhileKeepingAttributes(string: newtext), for: .normal)

So I ended up with this extension:

import UIKit

extension UILabel {
    func setTextWhileKeepingAttributes(string: String) {
        if let newAttributedText = self.attributedText {
            let mutableAttributedText = newAttributedText.mutableCopy()

            (mutableAttributedText as AnyObject).mutableString.setString(string)

            self.attributedText = mutableAttributedText as? NSAttributedString
        }
    }
    func getTextWhileKeepingAttributes(string: String) -> NSAttributedString {
        if let newAttributedText:NSAttributedString = self.attributedText {
            let mutableAttributedText = newAttributedText.mutableCopy()

            (mutableAttributedText as AnyObject).mutableString.setString(string)
            return mutableAttributedText as! NSAttributedString
        }
        else {
            // No attributes in this label, just create a new attributed string?
            let attributedstring = NSAttributedString.init(string: string)
            return attributedstring
        }
    }
}
Community
  • 1
  • 1
Jonny
  • 15,955
  • 18
  • 111
  • 232
  • doesn't work, attributes are gone when I set new text string for UILabel with using setTextWhileKeepingAttributes – user25 Feb 09 '18 at 09:17
  • yes. It doesn't work anymore. Anybody knows why? or the correct way? – luciano.bustos Apr 08 '18 at 05:17
  • 1
    I agree. It doesn't seem to work. And this is still running in legacy code. Without having time to deal with it at this point I, as the author of this answer, would advice against using it for now – Jonny Apr 08 '18 at 08:51
1

Changing the text of a mutable string will not do the jobs, since it will only keep the attributes of the first character and apply this to all of the text. Which seems to be by design, since it is part of the documentation.

So if you want to copy all attributes or change the string, you need to copy all attributes manually. Then you can create a MutableAttributedString and change the text. Afterwards you apply all the attributes to the new MutableAttributedString.

I have done it this way for Xamarin (in C#), but I think you can easily understand it and adapt it for your language:

NSMutableAttributedString result = new 
NSMutableAttributedString(attrStr.Value.Replace(blackSquare, bullet));
// You cannot simply replace an AttributedString's string, because that will discard attributes. 
// Therefore, I will now copy all attributes manually to the new MutableAttributedString:
NSRange outRange = new NSRange(0, 0);
int attributeIndex = 0;
while (outRange.Location + outRange.Length < attrStr.Value.Length   // last attribute range reached
            && attributeIndex < attrStr.Value.Length)                    // or last character reached
{
       // Get all attributes for character at attributeIndex
       var attributes = attrStr.GetAttributes(attributeIndex, out outRange);
       if (attributes != null && attributes.Count > 0)
       {
               result.AddAttributes(attributes, outRange); // copy all found attributes to result
               attributeIndex = (int)(outRange.Location + outRange.Length); // continue with the next range
       }
       else
       {
               attributeIndex++; // no attribues at the current attributeIndex, so continue with the next char
       }

}
// all attributes are copied
Viktor
  • 43
  • 7
0

None of the answers worked for me, but this one;

extension UILabel{
func setTextWhileKeepingAttributes(_ string: String) {
        if let attributedText = self.attributedText {
            let attributedString = NSMutableAttributedString(string: string,
                                                             attributes: [NSAttributedString.Key.font: font])
            attributedText.enumerateAttribute(.font, in: NSRange(location: 0, length: attributedText.length)) { (value, range, stop) in
                let attributes = attributedText.attributes(at: range.location, effectiveRange: nil)
                attributedString.addAttributes(attributes, range: range)
            }
            self.attributedText = attributedString
        }
    }
}
seymatanoglu
  • 151
  • 1
  • 9