Skip to content

Instantly share code, notes, and snippets.

@shadowfacts
Last active April 16, 2024 21:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save shadowfacts/07adf317972f2c06752d7dbb9c73308e to your computer and use it in GitHub Desktop.
Save shadowfacts/07adf317972f2c06752d7dbb9c73308e to your computer and use it in GitHub Desktop.
//
// ContentView.swift
// HSplitView
//
// Created by Shadowfacts on 4/16/24.
//
import SwiftUI
struct ContentView: View {
var body: some View {
MyHSplitView {
Color.red.opacity(0.5)
.overlay { Text("Left") }
Color.blue.opacity(0.5)
.overlay { Text("Middle") }
Color.green.opacity(0.5)
.overlay { Text("Right") }
}
}
}
struct MyHSplitView<Content: View>: View {
@ViewBuilder let content: Content
@State var columnCount: Int?
@State var splitFractions: [CGFloat] = []
let gap: CGFloat = 4
var body: some View {
HSplitLayout(gap: gap, columnCount: $columnCount, splitFractions: $splitFractions) {
content
dividers
}
}
private var dividers: some View {
GeometryReader { proxy in
ForEach(Array($splitFractions.enumerated()), id: \.offset) { (idx, $frac) in
Color.black
.frame(width: gap)
.position(x: proxy.size.width * frac, y: proxy.size.height / 2)
.gesture(DragGesture().onChanged({ value in
// better UX would be to clamp to some min fraction for each column
frac = value.location.x / proxy.size.width
}))
}
}
}
struct HSplitLayout: Layout {
let gap: CGFloat
@Binding var columnCount: Int?
@Binding var splitFractions: [CGFloat]
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
proposal.replacingUnspecifiedDimensions()
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
if columnCount != subviews.count - 1 {
// this is bad and hacky, but I think it's the best you can do without resorting to _VariadicView
DispatchQueue.main.async {
columnCount = subviews.count - 1
if subviews.count > 1 {
let frac = 1 / CGFloat(columnCount!)
splitFractions = (1..<columnCount!).map { CGFloat($0) * frac }
}
}
return
}
guard subviews.count > 1 else {
return
}
for (idx, subview) in subviews.dropLast().enumerated() {
let prevFrac = idx == 0 ? 0 : splitFractions[idx - 1]
let widthFrac = (idx == splitFractions.count ? 1 : splitFractions[idx]) - prevFrac
subview.place(
at: CGPoint(x: bounds.minX + bounds.width * prevFrac + gap / 2, y: bounds.minY),
proposal: ProposedViewSize(width: bounds.width * widthFrac - gap / 2, height: bounds.height)
)
}
// last subview is assumed to be the dividers
subviews.last!.place(at: bounds.origin, proposal: proposal)
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment