You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用单个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)
            }
        }
    }
}

方案关键点

  1. 主滚动视图控制:用NSScrollView作为外层,仅显示一个水平滚动条,负责整体的水平滚动控制。
  2. 滚动偏移同步:通过Coordinator监听两个NSTextView各自的滚动视图偏移,实时同步两者的水平位置,避免循环同步。
  3. Auto Layout约束:确保两个文本视图宽度一致、垂直铺满,并排显示,保证视觉对齐。
  4. 文本视图配置:禁止文本自动换行,允许垂直滚动,确保内容可以横向无限延伸。

内容的提问来源于stack exchange,提问作者Mgill404

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 22:47:03