2

I have a TextView in which I select some text and apply attributes to the selected text, successfully.

After the update of the NSMutableAttributedString with the desired changes I take my TextView and update its attributed text:

textView.attributedText = NSMutableAttributedStringText // pseudo-example

But this attribution replaces the whole text of the Text View (keeping the previous attributes ofc);

Is there any way of just updating the textView.attributedText change, instead of replacing the whole text every time I've made a change?

Andrew Li
  • 55,805
  • 14
  • 125
  • 143
Ivan Cantarino
  • 3,058
  • 4
  • 34
  • 73
  • Well what I wanted to do would be something like the regular textView.replace method, but instead of the String parameter it would be NSAttributedString. I might have to implement an extension – Ivan Cantarino Feb 08 '17 at 01:09
  • sorry I misread the question deleted my comment, I have not the solution for this but I think you should look into **TextKit **https://www.raywenderlich.com/77092/text-kit-tutorial-swift (old tutorial) for a solution to this problem –  Feb 08 '17 at 01:10
  • I'll have a look on that for sure :) – Ivan Cantarino Feb 08 '17 at 01:11
  • Should solve your issues I think, GL :) –  Feb 08 '17 at 01:12
  • Can you assign an NSMutableAttributedString, to UITextView.attributedString, modify mutable orig and dynamically update view by calling textView invalidate*() method or `textView.layoutManager.processEditing(for: textView.textStorage, edited: .editedAttributes, range: range, changeInLength: 0, invalidatedRange: range)`. Nope! UITextView, apparently, holds a distinct *copy* of your object. UIKit Text Kit doc says: "Most of the time, you can use TextKit to fine tune the formatting and layout of a UITextView by modifying the view’s textContainer, layoutManager, or textStorage properties" – clearlight May 11 '22 at 21:46

2 Answers2

3

I just did this earlier this week.

Create a mutable copy of attributedText, update the mutable copy, create an immutable copy of the updated string.

guard let text = textView.attributedText?.mutableCopy() as? NSMutableAttributedString else { return }
text.addAttribute(NSForegroundColorAttributeName, value: color, range: selectedRange)
textView.attributedText = text.copy() as? NSAttributedString
Jeffery Thomas
  • 42,202
  • 8
  • 92
  • 117
0

This subclass of UITextView let you work with UITextView and access the backing store's NSMutableAttributeString, where you to update string ranges. It will also refresh the layout dynamically, rather than have to replace the string wholesale by assigning a new string to UITextView.attributedString, wherein you're starting over and lose the user's text selection.

You can still set attributedText to update the backing store, but if you want to modify string, access it as a mutable attribute string through text view instance's mutableAttributedText property, for example:

This was confusing and cost me a day of headaches. For example, some of the UITextView fields become vestigial by default as soon as you provide your own backing store and layout, and it's tough to sort it out so I thought I'd save people the trouble.

 var textView = DynamicLayoutTextView()
 textView.attributedText = NSAttributedString(string: "Now what?")
 textView.mutableAttrText.setAttributes(attrs: attrs, range: range) 
 .
 .
 .


import UIKit

class DynamicLayoutTextView : UITextView {

    var dynamicStorage = DynamicLayoutTextStorage()

    override var attributedText : NSAttributedString? {
        get { dynamicStorage }
        set { dynamicStorage.setAttributedString(newValue!) }
    }

    var mutableAttributedText : NSMutableAttributedString? {
        get { dynamicStorage }
    }

    class DynamicLayoutTextStorage : NSTextStorage {
    
        let backingStore = NSMutableAttributedString()

        override var string: String {
            return backingStore.string
        }

        override func attributes(at location: Int,
                effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key : Any] {
            let attributes = backingStore.attributes(at: location, effectiveRange: range)
            return attributes
        }
        
        override func replaceCharacters(in range: NSRange, with str: String) {
            beginEditing()
            backingStore.replaceCharacters(in: range, with:str)
            edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
            endEditing()
        }
          
        override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
            beginEditing()
            backingStore.setAttributes(attrs, range: range)
            edited(.editedAttributes, range: range, changeInLength: 0)
            endEditing()
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("Not designed to be constructed by storyboard")
    }

    override init(frame: CGRect, textContainer: NSTextContainer?) {
        /*
         * Note: We're bypassing UITextView's attributedString, textLayout, layoutManager and replacing it with
         * components we manage. In the consumer, therefore it's important to update the dynamicStorage field to make changes
         * to the data and attributes.
         */
        textContainer!.widthTracksTextView = true
        let layoutManager = NSLayoutManager()
        layoutManager.addTextContainer(textContainer!)
        super.init(frame: frame, textContainer: textContainer)
        dynamicStorage.addLayoutManager(layoutManager)
    }

    convenience init(frame: CGRect) {
        let textContainer = NSTextContainer(size: CGSize(width: frame.size.width, height: .greatestFiniteMagnitude))
        self.init(frame: frame, textContainer: textContainer)
    }
}
clearlight
  • 12,255
  • 11
  • 57
  • 75