自定义带行号CodeEditor切换文件时数据异常问题求助
问题描述
- 基于SwiftUI/UIKit实现的带行号自定义编辑器,使用绑定关联Data数组中的文件内容时,切换选中文件(变量
i变化)后,修改内容会错误覆盖原有数据,但原生TextEditor无此问题。 - 绑定代码如下:
let binding = Binding( get: { String(data: files[i].content, encoding: .utf8) ?? "" }, set: { files[i].content = $0.data(using: .utf8) ?? Data() } ) CodeEditor(text: binding)
问题根源
- 全局TextView实例复用:
CodeEditor结构体中直接声明var textView = LineNumberedTextView()作为属性,SwiftUI的UIViewRepresentable是值类型,每次视图更新(如i变化)时结构体实例可能重建,但该textView会被意外复用,导致新旧绑定的文本数据串混。 - Coordinator持有旧实例:Coordinator保存的
parent是旧的CodeEditor结构体实例,切换文件后新的CodeEditor创建,但Coordinator仍可能操作旧绑定,导致数据写入错误位置。 - 选中行更新逻辑错误:
textViewDidChangeSelection中直接操作parent.textView,而非当前活跃的TextView实例,状态更新混乱。
修复方案及修改后代码
import SwiftUI struct CodeEditor: UIViewRepresentable { @Binding var text: String var colorScheme: ColorScheme = .dark var showLineNumbers: Bool = true func makeUIView(context: Context) -> LineNumberedTextView { // 每次创建新的TextView实例,避免复用 let textView = LineNumberedTextView() textView.isEditable = true textView.delegate = context.coordinator textView.autocorrectionType = .no textView.autocapitalizationType = .none textView.keyboardType = .asciiCapable textView.font = UIFont.monospacedSystemFont(ofSize: 15, weight: .regular) textView.backgroundColor = UIColor.white textView.textContainer.lineBreakMode = .byWordWrapping textView.colorScheme = colorScheme return textView } func updateUIView(_ uiView: LineNumberedTextView, context: Context) { // 同步文本时临时移除代理,避免触发不必要的绑定更新 if uiView.text != text { uiView.delegate = nil uiView.text = text uiView.delegate = context.coordinator } // 同步配色设置 if uiView.colorScheme != colorScheme { uiView.colorScheme = colorScheme uiView.setNeedsDisplay() } } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UITextViewDelegate { // 使用弱引用避免循环引用 weak var parent: CodeEditor? init(_ parent: CodeEditor) { self.parent = parent } func textViewDidChange(_ textView: UITextView) { guard let parent = parent, parent.text != textView.text else { return } parent.text = textView.text } func textViewDidChangeSelection(_ textView: UITextView) { // 直接操作当前TextView实例,而非parent中的旧实例 guard let numberedTextView = textView as? LineNumberedTextView else { return } numberedTextView.currentLines = textView.text.lineNumbersForRange(textView.selectedRange) ?? [] numberedTextView.setNeedsLayout() } } } // 以下辅助函数及扩展无需修改 func findRegex(pattern: String, text: String) -> [NSRange] { var result: [NSRange] = [] if let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) { let matches = regex.matches(in: text, options: [], range: NSRange(location: 0, length: text.count)) for match in matches { result.append(match.range) } } return result } 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] = [] let linesInRange = substring.components(separatedBy: .newlines) let lineBreaksBeforeStartIndex = self[..<startIndex].components(separatedBy: .newlines).count - 1 for (index, _) in linesInRange.enumerated() { let lineNumber = lineBreaksBeforeStartIndex + index + 1 lineNumbers.append(lineNumber) } return lineNumbers } } class LineNumberedTextView: UITextView { let lineNumberGutterWidth: CGFloat = 50 var currentLines: [Int] = [] var showLineNumbers: Bool = true var colorScheme: ColorScheme = .light override func draw(_ rect: CGRect) { let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.alignment = .left let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.monospacedSystemFont(ofSize: 15, weight: .ultraLight), .paragraphStyle: paragraphStyle, .foregroundColor: colorScheme == .light ? UIColor(white: 0.8, alpha: 1) : UIColor(white: 0.2, alpha: 1) ] let attributes_active: [NSAttributedString.Key: Any] = [ .font: UIFont.monospacedSystemFont(ofSize: 15, weight: .ultraLight), .paragraphStyle: paragraphStyle, .foregroundColor: colorScheme == .light ? UIColor.black : UIColor.white ] var lineNum = 1 var lineRect = CGRect.zero let full = NSRange(location: 0, length: self.textStorage.length) var isWrapped = false layoutManager.enumerateLineFragments(forGlyphRange: full) { (rect, usedRect, textContainer, glyphRange, stop) in let charRange: NSRange = self.layoutManager.glyphRange(forCharacterRange: glyphRange, actualCharacterRange: nil) let paraRange: NSRange? = (self.textStorage.string as NSString?)?.paragraphRange(for: charRange) let wrapped = charRange.location == paraRange?.location if wrapped { lineRect = CGRect(x: 8, y: rect.origin.y + self.textContainerInset.top, width: self.lineNumberGutterWidth, height: rect.height) let attributedString = NSAttributedString(string: "\(lineNum)", attributes: self.currentLines.contains(lineNum) ? attributes_active : attributes) attributedString.draw(in: lineRect) lineNum += 1 } isWrapped = !wrapped } if self.textStorage.string.isEmpty { let attributedString = NSAttributedString(string: "\(lineNum)", attributes: self.currentLines.contains(lineNum) ? attributes_active : attributes) attributedString.draw(at: CGPoint(x: 8, y: self.textContainerInset.top)) } if self.textStorage.string.hasSuffix("\n") { let rect = lineRect.offsetBy(dx: 0, dy: isWrapped ? (lineRect.height * 2) : lineRect.height) let attributedString = NSAttributedString(string: "\(lineNum)", attributes: self.currentLines.contains(lineNum) ? attributes_active : attributes) attributedString.draw(in: 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() } } }
关键修改说明
- 移除全局TextView实例:每次
makeUIView时创建新的LineNumberedTextView,确保切换文件时使用独立的编辑器实例,避免数据串用。 - Coordinator使用弱引用:防止循环引用,确保旧的
CodeEditor实例能被正确释放。 - 优化updateUIView逻辑:临时移除代理再设置文本,避免触发不必要的
textViewDidChange,防止循环更新;同时同步配色设置。 - 修正选中行更新逻辑:直接操作当前活跃的
LineNumberedTextView实例,确保状态更新准确。
内容的提问来源于stack exchange,提问作者Karl Ehrlich
相关产品推荐
相关产品推荐

