'iOS Swift - Applying attribute to text view without replacing whole text

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?



Solution 1:[1]

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

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 clearlight