我有这个 Uiview 可表示的行号,它工作得足够好,但是当一行环绕行号时,不会检测到这一点。我使用了“enumerateLineFragments”,但它使用换行作为单独的行。我对 UIKit 很陌生,这是我能想到的最好的方法,因为开源库的工作效果不是特别好,而且非常复杂。预先感谢。
这是我的代码。我可以这样使用:“CodeEditor(text: $text)”
import SwiftUI
struct CodeEditor: UIViewRepresentable {
@Binding var text: String
var textView = LineNumberedTextView()
func makeUIView(context: Context) -> LineNumberedTextView {
textView.isEditable = true
textView.delegate = context.coordinator
textView.font = UIFont.monospacedSystemFont(ofSize: 15, weight: .regular)
textView.backgroundColor = UIColor.white
textView.textContainer.lineBreakMode = .byCharWrapping
return textView
}
func updateUIView(_ uiView: LineNumberedTextView, context: Context) {
if text != uiView.text {
uiView.text = text
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: CodeEditor
init(_ parent: CodeEditor) {
self.parent = parent
}
func textViewDidChange(_ textView: UITextView) {
self.parent.text = textView.text
}
func textViewDidChangeSelection(_ textView: UITextView) {
self.parent.textView.currentLines = textView.text.lineNumbersForRange(textView.selectedRange) ?? []
self.parent.textView.setNeedsLayout()
}
}
}
extension String {
func lineNumbersForRange(_ range: NSRange) -> [Int]? {
guard
let startIndex = self.index(self.startIndex, offsetBy: range.location, limitedBy: self.endIndex),
let endIndex = self.index(startIndex, offsetBy: range.length, limitedBy: self.endIndex)
else {
return nil
}
let substring = self[startIndex..<endIndex]
var lineNumbers: [Int] = []
// Find the lines within the range
let linesInRange = substring.components(separatedBy: .newlines)
// Calculate the number of newline characters before the start index
let lineBreaksBeforeStartIndex = self[..<startIndex].components(separatedBy: .newlines).count - 1
// Iterate over each line within the range
for (index, _) in linesInRange.enumerated() {
// Calculate the line number for each line within the range
let lineNumber = lineBreaksBeforeStartIndex + index + 1
lineNumbers.append(lineNumber)
}
return lineNumbers
}
}
class LineNumberedTextView: UITextView {
let lineNumberGutterWidth: CGFloat = 40
let lineNumberPadding: CGFloat = 5
var currentLines: [Int] = []
override func draw(_ rect: CGRect) {
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .right
let attributes: [NSAttributedString.Key: Any] = [
.font: UIFont.monospacedSystemFont(ofSize: 15, weight: .ultraLight),
.paragraphStyle: paragraphStyle,
.foregroundColor: UIColor(white: 0.8, alpha: 1)
]
let attributes_active: [NSAttributedString.Key: Any] = [
.font: UIFont.monospacedSystemFont(ofSize: 15, weight: .ultraLight),
.paragraphStyle: paragraphStyle,
.foregroundColor: UIColor.black
]
var lineNum = 1
var lineRect = CGRect.zero
layoutManager.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: textStorage.length)) { (rect, usedRect, textContainer, glyphRange, stop) in
print("Line \(lineNum): H: \(rect.height)")
lineRect = CGRect(x: 5, y: rect.origin.y + self.textContainerInset.top, width: self.lineNumberGutterWidth - self.lineNumberPadding, height: rect.height)
let attributedString = NSAttributedString(string: "\(lineNum)", attributes: self.currentLines.contains(lineNum) ? attributes_active : attributes)
attributedString.draw(in: lineRect)
lineNum += 1
}
if self.textStorage.string.isEmpty {
let attributedString = NSAttributedString(string: "\(lineNum)", attributes: self.currentLines.contains(lineNum) ? attributes_active : attributes)
attributedString.draw(at: CGPoint(x: self.lineNumberGutterWidth - self.lineNumberPadding - 5, y: self.textContainerInset.top))
}
if self.textStorage.string.hasSuffix("\n") {
let rect = lineRect.offsetBy(dx: 0, dy: lineRect.height)
let attributedString = NSAttributedString(string: "\(lineNum)", attributes: self.currentLines.contains(lineNum) ? attributes_active : attributes)
attributedString.draw(in: rect)
print(rect)
}
}
override func layoutSubviews() {
super.layoutSubviews()
textContainerInset = UIEdgeInsets(top: textContainerInset.top, left: lineNumberGutterWidth, bottom: textContainerInset.bottom, right: textContainerInset.right)
setNeedsDisplay()
}
override var text: String! {
didSet {
setNeedsDisplay()
}
}
}
不能像Coordinator(self)那样使用self,因为这里的self是一个值而不是对象引用。
在更新中,您需要使用更改后的绑定值在使用新绑定的协调器上设置类似新闭包的内容。
make 需要初始化 UIView,所以它只会发生一次,你不应该在 SwiftUI 中将对象初始化为 struct vars,这是内存泄漏。