ios - Replace/delete 个字符 [• ] 在 UITextView 中输入
ios - Replace/delete characters [• ] in UITextView on Enter
我有一个带有自定义 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)
}
上面的代码不起作用。我可能没有使用正确的范围。
这是我的全部代码。
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)
总而言之,当用户输入并停留在该行时,我只需要删除光标所在的单词。
如果你帮我解决这个问题,我将不胜感激。
更新
如何测试这段代码?
- 第一行加一个项目符号+一个space[•],写几个字然后输入。
- 从这一点开始,它会添加一个项目符号,因为前一行以项目符号开头。
找到以下修改后的函数。使用 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()
}
我有一个带有自定义 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)
}
上面的代码不起作用。我可能没有使用正确的范围。
这是我的全部代码。
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)
总而言之,当用户输入并停留在该行时,我只需要删除光标所在的单词。
如果你帮我解决这个问题,我将不胜感激。
更新
如何测试这段代码?
- 第一行加一个项目符号+一个space[•],写几个字然后输入。
- 从这一点开始,它会添加一个项目符号,因为前一行以项目符号开头。
找到以下修改后的函数。使用 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()
}