我在 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()
}
}
}
}
}
我能确定的最佳前进道路是制作自己的 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
}
}
}