我有一个带有自定义NSTextStorage的UITextView,在每次Enter之后,如果前一行有列表子弹,我就添加一个列表子弹。
当用户回车时,只有光标所在行的开头有一个列表子弹,我就删除列表子弹,并停留在该行上。
第一个功能按预期工作。但我很难理解如何删除列表子弹。
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) ,0)
replaceCharactersInRange(next, withString: "", selectedRangeLocationMove: text.last!.count)
}
是上面的代码不工作。我可能没有使用正确的Range。
这是我所有的代码。
class TextView: UITextView {
internal var storage: TextStorage!
var defaultAttributes: [NSAttributedString.Key: AnyObject] = [:]
override init(frame: CGRect, textContainer: NSTextContainer?) {
let container = (textContainer == nil) ? NSTextContainer() : textContainer!
container.widthTracksTextView = true
container.heightTracksTextView = true
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(container)
self.storage = TextStorage()
self.storage.addLayoutManager(layoutManager)
super.init(frame: .zero, textContainer: container)
self.textContainerInset = .init(top: 16, left: 16, bottom: 16, right: 16)
self.isScrollEnabled = true
self.storage.textView = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TextStorage: NSTextStorage {
var backingStore: NSMutableAttributedString = NSMutableAttributedString()
var textView: UITextView!
override var string: String {
return self.backingStore.string
}
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func attributes(at location: Int, effectiveRange range: NSRangePointer?) -> [NSAttributedString.Key: Any] {
return backingStore.attributes(at: location, effectiveRange: range)
}
override func replaceCharacters(in range: NSRange, with str: String) {
var listPrefix: String? = nil
if (TextUtils.isReturn(str: str)) {
let currentLine = TextUtils.startOffset(self.string, location: range.location).0
let separateds = currentLine.components(separatedBy: " ")
if separateds.first!.contains("•") && currentLine.trimmingCharacters(in: .whitespaces).count == 1 {
listPrefix = ""
}
else {
if separateds.count >= 2 {
if separateds.first!.contains("•") {
listPrefix = "• "
}
}
}
}
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
guard let prefix = listPrefix else {
return
}
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) ,0)
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: text.last!.count)
} else {
let newRange = NSMakeRange(textView.selectedRange.location + str.count, 0)
replaceCharactersInRange(newRange, withString: prefix, selectedRangeLocationMove: prefix.count)
}
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}
func replaceCharactersInRange(_ replaceRange: NSRange, withString str: String, selectedRangeLocationMove: Int) {
if textView.undoManager!.isUndoing {
textView.selectedRange = NSMakeRange(textView.selectedRange.location - selectedRangeLocationMove, 0)
replaceCharactersInRange(NSMakeRange(replaceRange.location, str.count), withString: "")
} else {
replaceCharactersInRange(replaceRange, withString: str)
textView.selectedRange = NSMakeRange(textView.selectedRange.location + selectedRangeLocationMove, 0)
}
}
}
extension NSMutableAttributedString {
func replaceCharactersInRange(_ range: NSRange, withString str: String) {
if isSafeRange(range) {
replaceCharacters(in: range, with: str)
}
}
func isSafeRange(_ range: NSRange) -> Bool {
if range.location < 0 {
return false
}
let maxLength = range.location + range.length
return maxLength <= string.count
}
}
class TextUtils {
class func isReturn(str: String) -> Bool {
return str == "\n"
}
class func isBackspace(str: String) -> Bool {
return str == ""
}
class func startOffset(_ string: String, location: Int) -> (String, Int) {
var offset: Int = 0
var word = NSString(string: string).substring(to: location)
let lines = string.components(separatedBy: "\n")
if lines.count > 0 {
let last = lines.last!
offset = word.count - last.count
word = last
}
return (word, offset)
}
}
我也用过 self.deleteCharacters(in:)
而不是下面的函数。也没有成功。
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: text.last!.count)
综上所述,我只需要在用户回车时删除光标所在的一个字,并在该行停留。
如果您能帮助我解决这个问题,我将非常感激。
如何测试这段代码?
查找以下修改的功能。用Xcode 11.4 iOS 13.4测试。
override func replaceCharacters(in range: NSRange, with str: String) {
var listPrefix: String? = nil
if (TextUtils.isReturn(str: str)) {
let currentLine = TextUtils.startOffset(self.string, location: range.location).0
let separateds = currentLine.components(separatedBy: " ")
if separateds.first!.contains("•") && currentLine.trimmingCharacters(in: .whitespaces).count == 1 {
listPrefix = ""
}
else {
if separateds.count >= 2 {
if separateds.first!.contains("•") {
listPrefix = "• "
}
}
}
}
beginEditing()
backingStore.replaceCharacters(in: range, with:str)
edited(.editedCharacters, range: range, changeInLength: (str as NSString).length - range.length)
endEditing()
guard let prefix = listPrefix else {
return
}
if prefix.isEmpty {
let text = string.split(separator: "\n")
let next = NSMakeRange(textView.selectedRange.location - (text.last!.count) - 1, text.last!.count + 1)
replaceCharactersInRange(next, withString: " ", selectedRangeLocationMove: 0)
} else {
let newRange = NSMakeRange(textView.selectedRange.location + str.count, 0)
replaceCharactersInRange(newRange, withString: prefix, selectedRangeLocationMove: prefix.count)
}
}
override func setAttributes(_ attrs: [NSAttributedString.Key: Any]?, range: NSRange) {
guard range.upperBound <= string.count else { return }
beginEditing()
backingStore.setAttributes(attrs, range: range)
edited(.editedAttributes, range: range, changeInLength: 0)
endEditing()
}