如何用单个ScrollView同步滚动多个NSTextView?
实现两个NSTextView的同步水平滚动(SwiftUI)
需求:将两个NSTextView并排放置,通过单个水平滚动条同步控制两者的水平滚动(而非将两个视图作为整体滚动)。
问题分析
原代码用SwiftUI的ScrollView包裹HStack,会把两个视图当成一个整体进行水平滚动,无法实现各自内容同步滚动的效果。正确的做法是让两个NSTextView独立显示内容,同时绑定它们的水平滚动偏移,共用一个滚动条。
解决方案代码
import SwiftUI // 封装包含两个同步滚动NSTextView的NSView struct SyncedTextViews: NSViewRepresentable { func makeNSView(context: Context) -> NSScrollView { // 创建主滚动视图,仅显示水平滚动条 let scrollView = NSScrollView() scrollView.hasVerticalScroller = false scrollView.hasHorizontalScroller = true scrollView.autohidesScrollers = false // 内容容器视图,用于放置两个文本视图 let contentView = NSView() scrollView.documentView = contentView // 创建并配置两个文本视图 let textView1 = makeConfiguredTextView() let textView2 = makeConfiguredTextView() // 添加到内容容器 contentView.addSubview(textView1) contentView.addSubview(textView2) // 设置Auto Layout约束 textView1.translatesAutoresizingMaskIntoConstraints = false textView2.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ // 文本视图1约束:垂直铺满容器,左对齐 textView1.topAnchor.constraint(equalTo: contentView.topAnchor), textView1.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), textView1.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), // 文本视图2约束:垂直铺满容器,与文本1右侧间距16,宽度与文本1一致 textView2.topAnchor.constraint(equalTo: contentView.topAnchor), textView2.leadingAnchor.constraint(equalTo: textView1.trailingAnchor, constant: 16), textView2.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), textView2.widthAnchor.constraint(equalTo: textView1.widthAnchor), // 内容容器尺寸约束:宽度为两个文本视图宽度加间距,高度与文本视图一致 contentView.widthAnchor.constraint(equalTo: textView1.widthAnchor, multiplier: 2, constant: 16), contentView.heightAnchor.constraint(equalTo: textView1.heightAnchor) ]) // 同步两个文本视图的水平滚动偏移 context.coordinator.setupScrollSync(textView1: textView1, textView2: textView2) return scrollView } func updateNSView(_ nsView: NSScrollView, context: Context) {} // 创建并配置NSTextView private func makeConfiguredTextView() -> NSTextView { let textView = CustomTextView() // 填充测试内容 var content = "" for i in 1...50 { content += String(repeating: "\(i) ", count: i) content += "\n" } textView.string = content // 配置文本视图:禁止自动换行,允许垂直滚动,内容宽度无限制 textView.textContainer?.widthTracksTextView = false textView.textContainer?.containerSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) textView.isVerticallyResizable = true textView.isHorizontallyResizable = false textView.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) // 隐藏文本视图自带的滚动条 if let enclosingScrollView = textView.enclosingScrollView { enclosingScrollView.hasHorizontalScroller = false enclosingScrollView.hasVerticalScroller = false } return textView } func makeCoordinator() -> Coordinator { Coordinator() } // 协调器:监听滚动偏移并同步 class Coordinator: NSObject { private var observer1: NSKeyValueObservation? private var observer2: NSKeyValueObservation? private var isSyncing = false // 避免循环同步 func setupScrollSync(textView1: NSTextView, textView2: NSTextView) { guard let scroll1 = textView1.enclosingScrollView, let scroll2 = textView2.enclosingScrollView else { return } // 监听第一个滚动视图的水平偏移 observer1 = scroll1.observe(\.documentVisibleRect) { [weak self] scroll, _ in guard let self = self, !self.isSyncing else { return } self.isSyncing = true scroll2.documentVisibleRect.origin.x = scroll.documentVisibleRect.origin.x self.isSyncing = false } // 监听第二个滚动视图的水平偏移 observer2 = scroll2.observe(\.documentVisibleRect) { [weak self] scroll, _ in guard let self = self, !self.isSyncing else { return } self.isSyncing = true scroll1.documentVisibleRect.origin.x = scroll.documentVisibleRect.origin.x self.isSyncing = false } } } // 自定义NSTextView,重写intrinsicContentSize以正确计算尺寸 class CustomTextView: NSTextView { override var intrinsicContentSize: NSSize { guard let tc = textContainer, let lm = layoutManager else { return .zero } lm.ensureLayout(for: tc) return lm.usedRect(for: tc).size } } } // 测试应用 struct TestApp: App { var body: some Scene { WindowGroup { // 外层添加垂直滚动条,适配文本内容高度 ScrollView([.vertical]) { SyncedTextViews() .frame(minHeight: 400) } } } }
方案关键点
- 主滚动视图控制:用NSScrollView作为外层,仅显示一个水平滚动条,负责整体的水平滚动控制。
- 滚动偏移同步:通过Coordinator监听两个NSTextView各自的滚动视图偏移,实时同步两者的水平位置,避免循环同步。
- Auto Layout约束:确保两个文本视图宽度一致、垂直铺满,并排显示,保证视觉对齐。
- 文本视图配置:禁止文本自动换行,允许垂直滚动,确保内容可以横向无限延伸。
内容的提问来源于stack exchange,提问作者Mgill404
相关产品推荐
相关产品推荐

