iOS Swift - 将属性应用于文本视图而不替换整个文本

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

我有一个 TextView,其中 select 一些文本并将属性应用到 selected 文本,成功了。

在使用所需更改更新 NSMutableAttributedString 后,我使用我的 TextView 并更新其属性文本:

textView.attributedText = NSMutableAttributedStringText // pseudo-example

但是这个属性替换了Text View的整个文本(保留了c之前的属性);

有没有什么方法可以只更新 textView.attributedText 更改,而不是每次进行更改时都替换整个文本?

我这周早些​​时候刚做的。

创建 attributedText 的可变副本,更新可变副本,创建更新字符串的不可变副本。

guard let text = textView.attributedText?.mutableCopy() as? NSMutableAttributedString else { return }
text.addAttribute(NSForegroundColorAttributeName, value: color, range: selectedRange)
textView.attributedText = text.copy() as? NSAttributedString

UITextView 的这个子类让您可以使用 UITextView 并访问后备存储的 NSMutableAttributeString,您可以在其中更新字符串范围。它还会动态刷新布局,而不必通过将新字符串分配给 UITextView.attributedString 来替换整个字符串,在这种情况下您将重新开始并丢失用户的文本选择。

您仍然可以设置 attributedText 来更新后备存储,但是如果您想修改字符串,请通过文本视图实例的 mutableAttributedText 属性 将其作为可变属性字符串访问,例如:

这让我很困惑,让我头疼了一天。例如,一旦您提供了自己的后备存储和布局,某些 UITextView 字段默认情况下就会变得残留,并且很难对其进行整理,所以我想我可以省去麻烦。

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