Skip to content

Instantly share code, notes, and snippets.

@timboudreau
Created May 25, 2026 23:58
Show Gist options
  • Select an option

  • Save timboudreau/780c2b6f91653cdb8c89332d11fbd90a to your computer and use it in GitHub Desktop.

Select an option

Save timboudreau/780c2b6f91653cdb8c89332d11fbd90a to your computer and use it in GitHub Desktop.
Showing views with inter-view spacers - needs a little more Swift compiler jujitsu to eliminate the commas when using
import SwiftUI
/// Wraps some number of views of heterogenous types, without using something performance-killing like AnyView.
fileprivate enum ViewTuple<A: View, B: View, C: View, D : View, E: View, F: View, G: View, H: View, I: View, J: View> {
case None
case Single(A)
case Double(A, B)
case Triple(A, B, C)
case Quadruple(A, B, C, D)
case Pentatuple(A, B, C, D, E)
case Sextuple(A, B, C, D, E, F)
case Septuple(A, B, C, D, E, F, G)
case Octuple(A, B, C, D, E, F, G, H)
case Nonuple(A, B, C, D, E, F, G, H, I)
/// Get a menu (or whatever) view of the content
@ViewBuilder func compactContent(_ caption : String) -> some View {
Menu(caption) {
switch self {
case .None: EmptyView()
case .Single(let a): a
case .Double(let a, let b):
a
b
case .Triple(let a, let b, let c):
a
b
c
case .Quadruple(let a, let b, let c, let d):
a
b
c
d
case .Pentatuple(let a, let b, let c, let d, let e):
a
b
c
d
e
case .Sextuple(let a, let b, let c, let d, let e, let f):
a
b
c
d
e
f
case .Septuple(let a, let b, let c, let d, let e, let f, let g):
a
b
c
d
e
f
g
case .Octuple(let a, let b, let c, let d, let e, let f, let g, let h):
a
b
c
d
e
f
g
h
case .Nonuple(let a, let b, let c, let d, let e, let f, let g, let h, let i):
a
b
c
d
e
f
g
h
i
}
}
}
}
struct TupleView<A: View, B: View, C: View, D : View, E: View, F: View, G: View, H: View, I: View, J: View, Sep : View>: View {
@Environment(\.menuText) private var menuText : MenuText
@Environment(\.tupleViewSpacing) private var hspacing : CGFloat
private let tuple: ViewTuple<A, B, C, D, E, F, G, H, I, J>
private let separator : () -> Sep
fileprivate init(tuple : ViewTuple<A, B, C, D, E, F, G, H, I, J>, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) {
self.tuple = tuple
self.separator = separator
}
func withSeparator<S: View>(@ViewBuilder _ sep : @escaping () -> S) -> TupleView<A, B, C, D, E, F, G, H, I, J, S> {
.init(tuple: tuple, separator: sep)
}
init(@ViewBuilder _ a : () -> A, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where B == EmptyView, C == EmptyView, D == EmptyView, E == EmptyView, F == EmptyView, G == EmptyView, H == EmptyView, I == EmptyView, J == EmptyView {
self.tuple = .Single(a())
self.separator = separator
}
// Why @autoclosure? The Swift compiler will *not* infer types through @ViewBuilder, but can through @autoclosure
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where C == EmptyView, D == EmptyView, E == EmptyView, F == EmptyView, G == EmptyView, H == EmptyView, I == EmptyView, J == EmptyView {
self.tuple = .Double(a(), b())
self.separator = separator
}
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, _ c : @autoclosure () -> C, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where D == EmptyView, E == EmptyView, F == EmptyView, G == EmptyView, H == EmptyView, I == EmptyView, J == EmptyView {
self.tuple = .Triple(a(), b(), c())
self.separator = separator
}
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, _ c : @autoclosure () -> C, _ d : @autoclosure () -> D, _ e : @autoclosure () -> E, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where E == EmptyView, F == EmptyView, G == EmptyView, H == EmptyView, I == EmptyView, J == EmptyView {
self.tuple = .Quadruple(a(), b(), c(), d())
self.separator = separator
}
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, _ c : @autoclosure () -> C, _ d : @autoclosure () -> D, _ e : @autoclosure () -> E, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where F == EmptyView, G == EmptyView, H == EmptyView, I == EmptyView, J == EmptyView {
self.tuple = .Pentatuple(a(), b(), c(), d(), e())
self.separator = separator
}
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, _ c : @autoclosure () -> C, _ d : @autoclosure () -> D, _ e : @autoclosure () -> E, _ f : @autoclosure () -> F, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where G == EmptyView, H == EmptyView, I == EmptyView, J == EmptyView {
self.tuple = .Sextuple(a(), b(), c(), d(), e(), f())
self.separator = separator
}
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, _ c : @autoclosure () -> C, _ d : @autoclosure () -> D, _ e : @autoclosure () -> E, _ f : @autoclosure () -> F, _ g : @autoclosure () -> G, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where H == EmptyView, I == EmptyView, J == EmptyView {
self.tuple = .Septuple(a(), b(), c(), d(), e(), f(), g())
self.separator = separator
}
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, _ c : @autoclosure () -> C, _ d : @autoclosure () -> D, _ e : @autoclosure () -> E, _ f : @autoclosure () -> F, _ g : @autoclosure () -> G, _ h : @autoclosure () -> H, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) where I == EmptyView, J == EmptyView {
self.tuple = .Octuple(a(), b(), c(), d(), e(), f(), g(), h())
self.separator = separator
}
init(_ a : @autoclosure () -> A, _ b : @autoclosure () -> B, _ c : @autoclosure () -> C, _ d : @autoclosure () -> D, _ e : @autoclosure () -> E, _ f : @autoclosure () -> F, _ g : @autoclosure () -> G, _ h : @autoclosure () -> H, _ i : @autoclosure () -> I, @ViewBuilder separator: @escaping () -> Sep = { DotSeparator() }) {
self.tuple = .Nonuple(a(), b(), c(), d(), e(), f(), g(), h(), i())
self.separator = separator
}
var body : some View {
ViewThatFits {
switch tuple {
case .None: EmptyView()
case .Single(let a): a
case .Double(let a, let b):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
}
.frame(maxWidth: .infinity)
case .Triple(let a, let b, let c):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
separator()
c.allowsTightening(true)
}
.frame(maxWidth: .infinity)
case .Quadruple(let a, let b, let c, let d):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
separator()
c.allowsTightening(true)
separator()
d.allowsTightening(true)
}
.frame(maxWidth: .infinity)
case .Pentatuple(let a, let b, let c, let d, let e):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
separator()
c.allowsTightening(true)
separator()
d.allowsTightening(true)
separator()
e.allowsTightening(true)
}
.frame(maxWidth: .infinity)
case .Sextuple(let a, let b, let c, let d, let e, let f):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
separator()
c.allowsTightening(true)
separator()
d.allowsTightening(true)
separator()
e.allowsTightening(true)
separator()
f.allowsTightening(true)
}
.frame(maxWidth: .infinity)
case .Septuple(let a, let b, let c, let d, let e, let f, let g):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
separator()
c.allowsTightening(true)
separator()
d.allowsTightening(true)
separator()
e.allowsTightening(true)
separator()
f.allowsTightening(true)
separator()
g.allowsTightening(true)
}.frame(maxWidth: .infinity)
case .Octuple(let a, let b, let c, let d, let e, let f, let g, let h):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
separator()
c.allowsTightening(true)
separator()
d.allowsTightening(true)
separator()
e.allowsTightening(true)
separator()
f.allowsTightening(true)
separator()
g.allowsTightening(true)
separator()
h.allowsTightening(true)
}.frame(maxWidth: .infinity)
case .Nonuple(let a, let b, let c, let d, let e, let f, let g, let h, let i):
HStack(spacing: self.hspacing) {
a.allowsTightening(true)
separator()
b.allowsTightening(true)
separator()
c.allowsTightening(true)
separator()
d.allowsTightening(true)
separator()
e.allowsTightening(true)
separator()
f.allowsTightening(true)
separator()
g.allowsTightening(true)
separator()
h.allowsTightening(true)
separator()
i.allowsTightening(true)
}.frame(maxWidth: .infinity)
}
// Alternate view - use a menu - ViewThatFits will pick this
// if the area is too narrow
tuple.compactContent(menuText.description)
}
}
}
/// Defines up the text used on the menu view of TupleViews that can't fit their width.
struct MenuText : Sendable, Equatable, Hashable, Identifiable, CustomStringConvertible {
let description : String
var id : String { description }
init(_ description: String) {
self.description = description
}
}
extension MenuText : ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.description = value
}
}
/// Looks up the text used on the menu view of TupleViews that can't fit their width.
struct MenuTextKey : EnvironmentKey {
static var defaultValue : MenuText = "Actions"
}
/// Looks up the spacing for the internal `HStack` in a TupleView
struct TupleViewSpacing : EnvironmentKey {
static var defaultValue : CGFloat = 8
}
extension EnvironmentValues {
var menuText: MenuText {
get {
self[MenuTextKey.self]
} set {
self[MenuTextKey.self] = newValue
}
}
var tupleViewSpacing : CGFloat {
get {
self[TupleViewSpacing.self]
} set {
self[TupleViewSpacing.self] = newValue
}
}
}
/// Alternate vertical line separator for TupleView
struct VerticalLineSeparator : View {
@Environment(\.colorScheme) private var scheme : ColorScheme
@Environment(\.dynamicTypeSize) private var typeSize : DynamicTypeSize
var body : some View {
Canvas { ctx, size in
var p = Path();
p.move(to: CGPoint(x: round(size.width / 2), y : 0))
p.addLine(to: CGPoint(x : round(size.width / 2), y : size.height))
ctx.stroke(p, with: .color(scheme.theme.bandsViewActiveParameterLegend), style: scheme.theme.bandsViewIndicatorRule)
}.frame(maxWidth: typeSize.separatorWidth, maxHeight: .infinity).fixedSize(horizontal: true, vertical: false)
}
}
/// Default separator for TupleView
struct DotSeparator : View {
@Environment(\.colorScheme) private var scheme : ColorScheme
@Environment(\.dynamicTypeSize) private var typeSize : DynamicTypeSize
var body : some View {
Canvas { ctx, size in
let width : CGFloat = typeSize.dotWidth
let rect = CGRect(x: -width / 2 + size.width / 2 , y: -width / 2 + size.height / 2, width: width, height: width)
ctx.stroke(Circle().path(in: rect), with: .color(scheme.theme.bandsViewActiveParameterLegend), style: scheme.theme.bandsViewIndicatorRule)
ctx.fill(Circle().path(in: rect), with: .color(scheme.theme.bandsViewActiveParameterLegend.opacity(0.75)))
}.frame(maxWidth: typeSize.separatorWidth, maxHeight: .infinity).fixedSize(horizontal: true, vertical: false)
}
}
fileprivate extension DynamicTypeSize {
var separatorWidth : CGFloat {
switch self {
case .large: 14
case .medium: 12
case .small: 10
case .xSmall: 8
case .xLarge: 16
case .xxLarge: 18
case .xxxLarge: 20
default: 24
}
}
var dotWidth : CGFloat {
self.separatorWidth * 0.125
}
}
// Borrowed from PluginLibs
extension ColorScheme {
var theme : Theme { .init() }
}
// Borrowed from PluginLibs
struct Theme {
let bandsViewActiveParameterLegend : Color = .orange
let bandsViewIndicatorRule = StrokeStyle(lineWidth: 0.425, lineCap: .round, lineJoin: .round, miterLimit: 3.0, dash: [], dashPhase: 1.0)
}
struct TupleViewDemo : View {
@State var whatever = false
var body : some View {
VStack {
Text("Tuple View Demo").font(.title)
TupleView(
Button("Button A"){
print("A clicked")
},
Button("Button B"){
print("B clicked")
},
Button("Button C"){
print("C clicked")
},
Text("Pwee!"),
Button("Button D"){
print("D clicked")
},
Button("Button E"){
print("E clicked")
},
Toggle("Toogle F!", isOn: $whatever),
Button("Button G"){
print("G clicked")
},
)
.environment(\.tupleViewSpacing, 0)
.buttonStyle(.borderless)
.foregroundStyle(Color.accentColor)
.frame(maxHeight: 24)
.padding()
Rectangle()
.foregroundStyle(.black)
.frame(idealWidth: .infinity, maxWidth: .infinity, idealHeight: .infinity, maxHeight: .infinity)
.overlay {
Text("This is a big black box.\n\nSome visualization goes here.").font(.title).bold().multilineTextAlignment(.center)
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment