用一个ScrollView滚动多个视图

问题描述 投票:0回答:1

我在 HStack 中有两个 NSTextView。每个文本视图的内容可以超出其水平空间。我想用一个水平滚动条控制两个文本视图。

这是 DiffMerge 执行我想做的事情的剪辑。 https://imgur.com/a/dJ9xduw

我一直在使用 SwiftUI 并拥有当前的设置。在此配置中,水平滚动条将作为一个巨大视图滚动穿过并排视图。这个结果对我来说很有意义,我只是不确定如何获得我想要的结果。

import SwiftUI

struct DumbedDownRepresentable : NSViewRepresentable {
    typealias NSViewType = NSTextView
    
    func makeNSView(context: Context) -> NSTextView {
        let textView = TextView()
        for i in 1...50 {
            textView.string += String(repeating: "\(i) ", count: i)
            textView.string += "\n"
        }

        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)
        return textView
    }
    
    func updateNSView(_ nsView: NSTextView, context: Context) {
        
    }
    
    func sizeThatFits(_ proposal: ProposedViewSize, nsView: UXCodeTextView, context: Context) -> CGSize? {
        var size = nsView.intrinsicContentSize
        if let proposedWidth = proposal.width {
            size.width = max(proposedWidth, size.width)
        }
        return size
    }
    
    class TextView: NSTextView {
        override var intrinsicContentSize: NSSize {
            if let tc = self.textContainer, let lm = self.layoutManager {
                lm.ensureLayout(for: tc)
                return lm.usedRect(for: tc).size
            }
            return .zero
        }
    }
}
import SwiftUI

struct TestApp: App {
    
    var body: some Scene {
        WindowGroup {
            ScrollView([.horizontal]) {
                HStack {
                    DumbedDownRepresentable()
                    DumbedDownRepresentable()
                }
            }
        }
    }
}
macos swiftui nsscrollview
1个回答
0
投票

我能确定的最佳前进道路是制作自己的 ScrollView。这个实现并不完整,但足以结束这个问题。

struct TestApp: App {
    @State var hProgress: CGFloat = 0.0
    @State var vProgress: CGFloat = 0.0
    
    var body: some Scene {
        WindowGroup {
            GeometryReader() { geometry in
                Container {
                    HStack(alignment: .top, spacing: 0) {
                        DumbedDownRepresentable(hProgress: $hProgress, vProgress: $vProgress)
                            .frame(minWidth: (geometry.size.width - 20)/2) // 18 is magic number for vertical scroller
                        DumbedDownRepresentable(hProgress: $hProgress, vProgress: $vProgress)
                            .frame(minWidth: (geometry.size.width - 20)/2) // 18 is magic number for vertical scroller
                    }
                }
                .onPreferenceChange(HProgressKey.self) { value in
                    self.hProgress = value
                }
                .onPreferenceChange(VProgressKey.self) { value in
                    self.vProgress = value
                }
            }
        }
    }
}


struct Container<Content: View>: View {
    @ViewBuilder var content: Content
    var indicatorWidth: CGFloat = 20
    
    var body: some View {
        GeometryReader() { geometry in
            VStack(alignment: .leading, spacing: 0) {
                HStack(alignment: .top, spacing: 0) {
                    content
                        .frame(height: geometry.size.height - indicatorWidth)
                    VerticalScrollBar()
                        .frame(height: geometry.size.height - indicatorWidth)
                }
                HorizontalScrollBar()
            }
        }
    }
}

struct VerticalScrollBar: View {
    @State var vOffset: CGFloat
    @State var barH: CGFloat = 0
    var barW: CGFloat = 20
    var indicatorH: CGFloat = 100
    var inset: CGFloat = 10
    
    init() {
        vOffset = inset
    }
    
    var body: some View {
        ZStack {
            Rectangle()
                .fill(.blue)
                .frame(maxWidth: barW, maxHeight: .infinity)
            RoundedRectangle(cornerSize: CGSize(width: 10, height: 10))
                .fill(.gray)
                .frame(width: barW/2, height: indicatorH)
                .gesture(DragGesture()
                    .onChanged() { value in
                        setVOffset(vOffset + value.translation.height)
                    }
                )
                .position(x: barW/2, y: indicatorH/2)
                .offset(y: vOffset)
                .preference(key:VProgressKey.self, value: progress())
                .onGeometryChange(for: CGSize.self) { proxy in
                    proxy.size
                } action: { newSize in
                    let progress = progress()
                    barH = newSize.height
                    setVOffset(progress * (barH-indicatorH-inset-inset))
                }
        }
    }
    
    func setVOffset(_ value: CGFloat) {
        vOffset = max(inset, value)
        vOffset = min(vOffset, barH-indicatorH-inset)
    }
    
    func progress() -> CGFloat {
        // TODO: divide by zero
        return (vOffset-inset) / (barH-indicatorH-inset-inset)
    }
}

struct HorizontalScrollBar: View {
    @State var hOffset: CGFloat
    @State var barW: CGFloat = 0
    var indicatorW: CGFloat = 100
    var barH: CGFloat = 20
    var inset: CGFloat = 10
    
    init() {
        hOffset = inset
    }
    
    var body: some View {
            ZStack {
                Rectangle()
                    .fill(.blue)
                    .frame(maxWidth: .infinity, maxHeight: barH)
                RoundedRectangle(cornerSize: CGSize(width: 10, height: 10))
                    .fill(.gray)
                    .frame(width: indicatorW, height: barH/2)
                    .gesture(DragGesture()
                        .onChanged() { value in
                            setHOffset(hOffset + value.translation.width)
                        }
                    )
                    .position(x: indicatorW/2, y: barH/2)
                    .offset(x: hOffset)
                    .preference(key:HProgressKey.self, value: progress())
                    .onGeometryChange(for: CGSize.self) { proxy in
                        proxy.size
                    } action: { newSize in
                        let progress = progress()
                        barW = newSize.width
                        setHOffset(progress * (barW-indicatorW-inset-inset))
                    }
        }
    }
    
    func setHOffset(_ value: CGFloat) {
        hOffset = max(inset, value)
        hOffset = min(hOffset, barW-indicatorW-inset)
    }
    
    func progress() -> CGFloat {
        return (hOffset-inset) / (barW-indicatorW-inset-inset)
    }
}

struct HProgressKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}
    
struct VProgressKey: PreferenceKey {
    static var defaultValue: CGFloat { 0 }
    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value = nextValue()
    }
}

import SwiftUI

struct DumbedDownRepresentable : NSViewRepresentable {
    typealias NSViewType = NSClipView
    @Binding var hProgress: CGFloat
    @Binding var vProgress: CGFloat
    
    func makeNSView(context: Context) -> NSClipView {
        let clipView = NSClipView()
        let textView = TextView()
        clipView.documentView = textView
        
        for i in 1...50 {
            textView.string += String(repeating: "\(i) ", count: i)
            textView.string += "\n"
        }
        
        textView.textContainer?.widthTracksTextView = false
        textView.textContainer?.containerSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        textView.isVerticallyResizable = true
        textView.isHorizontallyResizable = true
        textView.maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        return clipView
    }
    
    func updateNSView(_ clipView: NSClipView, context: Context) {
        print(hProgress, ", ", vProgress)
        let maxH = clipView.documentRect.width - clipView.frame.width
        let maxV = clipView.documentRect.height - clipView.frame.height
        clipView.scroll(to: .init(x: hProgress*maxH, y: vProgress*maxV))
    }
    
    func sizeThatFits(_ proposal: ProposedViewSize, nsView: UXCodeTextView, context: Context) -> CGSize? {
        var size = nsView.intrinsicContentSize
        if let proposedWidth = proposal.width {
            size.width = max(proposedWidth, size.width)
        }
        return size
    }
    
    class TextView: NSTextView {
        override var intrinsicContentSize: NSSize {
            if let tc = self.textContainer, let lm = self.layoutManager {
                lm.ensureLayout(for: tc)
                return lm.usedRect(for: tc).size
            }
            return .zero
        }
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.