TextView UIViewRepresentable设置行间距时UndoManager重置问题求助
I've run into exactly this issue before—trying to add line spacing to a UITextView wrapped in UIViewRepresentable breaks undo/redo, and messing with the text storage while keeping the SwiftUI binding synced can cause text resets on view refreshes. Here's a solid solution that fixes both problems:
Core Approach
Instead of replacing the entire attributedText (which nukes the UndoManager), we modify the existing textStorage directly to apply line spacing. We also add guard clauses to avoid unnecessary text resets during view updates, and handle the SwiftUI binding sync correctly without breaking the undo stack.
Full Implementation
import SwiftUI import UIKit struct TextView: UIViewRepresentable { @Binding var text: String var lineSpacing: CGFloat = 0 func makeUIView(context: Context) -> UITextView { let textView = UITextView() textView.delegate = context.coordinator textView.font = UIFont.systemFont(ofSize: 16) // Apply initial line spacing context.coordinator.updateLineSpacing(for: textView, lineSpacing: lineSpacing) return textView } func updateUIView(_ uiView: UITextView, context: Context) { // Only update text if it's actually different—prevents resets on view refreshes guard uiView.text != text else { return } // Temporarily disable undo registration to avoid adding programmatic text updates to the undo stack uiView.undoManager?.disableUndoRegistration() uiView.text = text uiView.undoManager?.enableUndoRegistration() // Refresh line spacing after updating text context.coordinator.updateLineSpacing(for: uiView, lineSpacing: lineSpacing) } func makeCoordinator() -> Coordinator { Coordinator(self) } class Coordinator: NSObject, UITextViewDelegate { var parent: TextView init(_ parent: TextView) { self.parent = parent } func textViewDidChange(_ textView: UITextView) { // Sync text changes back to SwiftUI binding—this doesn't break undo parent.text = textView.text // Ensure new text gets the correct line spacing updateLineSpacing(for: textView, lineSpacing: parent.lineSpacing) } func updateLineSpacing(for textView: UITextView, lineSpacing: CGFloat) { guard let textStorage = textView.textStorage else { return } let fullTextRange = NSRange(location: 0, length: textStorage.length) // Clear existing paragraph style to avoid conflicts textStorage.removeAttribute(.paragraphStyle, range: fullTextRange) // Create and apply new paragraph style with line spacing let paragraphStyle = NSMutableParagraphStyle() paragraphStyle.lineSpacing = lineSpacing paragraphStyle.alignment = textView.textAlignment textStorage.addAttribute(.paragraphStyle, value: paragraphStyle, range: fullTextRange) } } }
Key Fixes Explained
- Avoid Unnecessary Text Resets: The
guard uiView.text != text else { return }check inupdateUIViewensures we don't overwrite the text view's content during unrelated view refreshes (like switching apps or rotating the screen). - Preserve Undo Stack: By modifying the existing
textStorageinstead of replacingattributedText, we don't trigger a reset of the text view's internalUndoManager. We also temporarily disable undo registration when updating text programmatically to avoid cluttering the undo stack. - Sync Binding Without Issues: The
textViewDidChangemethod keeps the SwiftUI binding synced to the text view's content. Since this runs after the user's text changes, it doesn't interfere with undo/redo functionality.
How to Use It
Test the solution with a simple SwiftUI view:
struct ContentView: View { @State private var text = "Hello, this is a test text with line spacing.\n\nMultiple lines should have the correct spacing between them. Try typing some text and using undo/redo—it should work perfectly!" var body: some View { NavigationStack { TextView(text: $text, lineSpacing: 8) .padding() .border(Color.gray) .navigationTitle("Line Spacing + Undo Fix") } } }
This implementation keeps line spacing working, preserves undo/redo functionality, and prevents text resets during view updates—all the boxes checked!
内容的提问来源于stack exchange,提问作者Barrrdi

