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