1

I have a UITextView in an iOS app, and I want to allow the user to bold/italicize/underline/etc at will, so I gave set the allowsEditingTextAttributes property to true on the text view. However, I would also like to allow the user to change things like font and color. Currently, it seems that changing even the color property of the UITextView resets the formatting of the UITextView (i.e. gets rid of the bolding/italics/etc). Is there any way to change this without resetting the formatting? For reference, I am currently doing this to change the color:

self.textView.textColor = colorTheUserPicked //a UIColor picked by the user

EDIT: Actually, this was my bad, I was also resetting the value of the text property itself when I changed the color. Removing that allowed me to change the color as desired. However, I still can't change the font or even the font size without removing the bold/italics/etc. I realize this might be impossible, though...

Ruben Martinez Jr.
  • 3,199
  • 5
  • 42
  • 76
  • 1
    Bold/Italic are values inside the `NSFontAttributedName`. Since it's a `NSDictionary`, you can't change them like this. You'd have to enumerate the attributes and change them manually. – Larme Mar 09 '15 at 14:49
  • Okay, how might that look? – Ruben Martinez Jr. Mar 10 '15 at 10:51
  • 1
    You can have a look there to how enumerate (it's in Objective-C, but you can translate it easily in Swift I think): http://stackoverflow.com/questions/23153156/find-attributes-from-attributed-string-that-user-typed/23153221#23153221 – Larme Mar 10 '15 at 13:23

3 Answers3

2

I'll answer with an Objective-C solution since I don't code in Swift, but it should be translated easily into Swift.

NSAttributedString "effects" are stored in a NSDictionary. So it's a Key:Value system (with unicity of Key).
Bold/Italic (from your example) are inside the NSFontAttributedName's value. That's why you can't set it again like this.

The main key is to use enumerateAttributesInRange:options:usingBlock::

[attr enumerateAttributesInRange:NSMakeRange(0, [attr length])
                         options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired
                      usingBlock:^(NSDictionary *attributes, NSRange range, BOOL *stop)
 {
     NSMutableDictionary *newAttributes = [attributes mutableCopy];
    //Do changes
    [attr addAttributes:newAttributes range:range]; //Apply effects
 }];

The following lines are at "//Do changes" place. So, if you want to change the Font size:

if ([newAttributes objectForKey:NSFontAttributeName])
{
    UIFont *currentFont = (UIFont *)[attributes objectForKey:NSFontAttributeName];
    UIFont *newFont = [UIFont fontWithName:[currentFont fontName] size:[currentFont pointSize]*0.5];//Here, I multiplied the previous size with 0.5
    [newAttributes setValue:newFont forKey:NSFontAttributeName];
}

If you want to bold/italic, etc. the font you may be interested in UIFontDescriptor, and a related question.

It shows you how to get the previous font and change get a new font with some commons with the previous one.

If you want to change the color:

if ([newAttributes objectForKey:NSForegroundColorAttributeName])//In case if there is some part of the text without color that you don't want to override, or some custom- things
{
     UIColor *newColor = [UIColor blueColor];
    [newAttributes setValue:newColor forKey:NSForegroundColorAttributeName];
}

The underline effect is in NSUnderlineStyleAttributeName's value. So analogic changes can be done.

Community
  • 1
  • 1
Larme
  • 24,190
  • 6
  • 51
  • 81
  • Thank you! Accepted yours, and posted Swift translation below. – Ruben Martinez Jr. Mar 10 '15 at 17:11
  • Actually, this has an issue: Once this change is made, the "default" font new (additional) text appears in is unchanged. How can I change this? If I just change the textView.font value, it undoes the purpose of this workaround and removes the formatting. Ideas? – Ruben Martinez Jr. Mar 10 '15 at 19:56
  • Edit: it turns out if I set the font value, and *then* set the attributedText property of my textView, that would do what I wanted without losing formatting. – Ruben Martinez Jr. Mar 10 '15 at 20:09
1

Thanks @Larme! Your solution is correct, so I accepted it! Here is the Swift version for reference:

//needs to be a mutable attributed string
var mutableCopy = NSMutableAttributedString(attributedString: textView.attributedText)

//make range
var textRange = NSMakeRange(0, textView.attributedText.length)

//get all the string attributes
textView.attributedText.enumerateAttributesInRange(textRange, options: NSAttributedStringEnumerationOptions.LongestEffectiveRangeNotRequired, usingBlock: { (attributes, range, stop) -> Void in
    //make a copy of the attributes we can edit
    var newAttributes = attributes as [NSObject : AnyObject]
    if newAttributes[NSFontAttributeName] != nil { //if the font attr exists
        //create a new font with the old font name and new size
        let currentFont = newAttributes[NSFontAttributeName] as UIFont
        let newFont = UIFont(name: currentFont.fontName, size: currentFont.pointSize * 0.5) //set new font size to half of old size
        //replace the nsfontattribute's font with the new one
        newAttributes[NSFontAttributeName] = newFont
    }
    //replace the old attributes with the new attributes to the mutable version
    mutableCopy.addAttributes(newAttributes, range: range)
})
//replace the old attributed text with the newly attributed text
textView.attributedText = mutableCopy
Ruben Martinez Jr.
  • 3,199
  • 5
  • 42
  • 76
1

Implemented similar for C# for Xamarin.IOS.

    NSError error = null;
        var htmlString = new NSAttributedString(NSUrl.FromFilename(
            "About.html"),
            new NSAttributedStringDocumentAttributes {DocumentType = NSDocumentType.HTML},
            ref error);

        var mutableCopy = new NSMutableAttributedString(htmlString);

        htmlString.EnumerateAttributes(new NSRange(0, htmlString.Length),
            NSAttributedStringEnumeration.LongestEffectiveRangeNotRequired,
            (NSDictionary attributes, NSRange range, ref bool stop) =>
            {
                var newMutableAttributes = new NSMutableDictionary(attributes);
                if (newMutableAttributes[UIStringAttributeKey.Font] != null)
                {
                    var currentFont = newMutableAttributes[UIStringAttributeKey.Font] as UIFont;
                    if (currentFont != null)
                    {
                            var newFontName = currentFont.Name;

                            if (currentFont.Name.Equals("TimesNewRomanPS-BoldItalicMT"))
                            {
                                newFontName = "HelveticaNeue-BoldItalic";
                            }
                            else if (currentFont.Name.Equals("TimesNewRomanPS-ItalicMT"))
                            {
                                newFontName = "HelveticaNeue-Italic";
                            }
                            else if (currentFont.Name.Equals("TimesNewRomanPS-BoldMT"))
                            {
                                newFontName = "HelveticaNeue-Bold";
                            }
                            else if (currentFont.Name.Equals("TimesNewRomanPSMT"))
                            {
                                newFontName = "HelveticaNeue";
                            }

                            newMutableAttributes.SetValueForKey(UIFont.FromName(newFontName, currentFont.PointSize * 1.5f), UIStringAttributeKey.Font); //  = newFont;
                    }
                    else
                    {
                        newMutableAttributes.SetValueForKey(UIFont.FromName("HelveticaNeue", 14), UIStringAttributeKey.Font); //  Default to something.
                    }
                }

                mutableCopy.AddAttributes(newMutableAttributes, range);
            });

        aboutTextView.AttributedText = mutableCopy;
Malmo
  • 15
  • 8