1

我有一个带有自定义 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)

总而言之,我只需要在用户输入并停留在该行时删除光标所在的单词。

如果您能帮我解决这个问题,我将不胜感激。

更新

如何测试这段代码?

  1. 在第一行,添加一个项目符号+一个空格[•],写几个字并输入。
  2. 从这一点开始,它添加了一个项目符号,因为前一行以项目符号开头。
4

1 回答 1

1

在下面找到修改后的功能。使用 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()
}
于 2020-05-14T11:03:43.280 回答