Skip to content

Instantly share code, notes, and snippets.

@nyteshade
Created May 28, 2026 18:25
Show Gist options
  • Select an option

  • Save nyteshade/12df01a338ef93723858ec023333c510 to your computer and use it in GitHub Desktop.

Select an option

Save nyteshade/12df01a338ef93723858ec023333c510 to your computer and use it in GitHub Desktop.
Alt Approach
import SwiftUI
/// A row of heterogeneous views separated by a configurable separator,
/// collapsing into a `Menu` when the available width is too small to lay
/// the items out side-by-side.
///
/// Backed by Swift 5.9 parameter packs (Xcode 15, iOS 17, macOS 14), so
/// any arity is supported without per-arity initializer overloads.
struct TupleView<First: View, each Rest: View, Sep: View>: View {
@Environment(\.menuText) private var menuText: MenuText
@Environment(\.tupleViewSpacing) private var hspacing: CGFloat
private let first: First
private let rest: (repeat each Rest)
private let separator: () -> Sep
private let interleavedItems: [AnyView] // Cached interleaved array
/// Creates a `TupleView` from one or more child views.
///
/// - Parameters:
/// - first: The leading view. No separator is drawn before it.
/// - rest: Zero or more additional views; each is preceded by a
/// separator in the laid-out `HStack`.
/// - separator: View-builder producing the separator drawn between
/// adjacent items. Defaults to `DotSeparator`.
init(
_ first: First,
_ rest: repeat each Rest,
@ViewBuilder separator: @escaping () -> Sep = { DotSeparator() },
) {
self.first = first
self.rest = (repeat each rest)
self.separator = separator
self.interleavedItems = Self.interleave(first: first, rest: repeat each rest, separator: separator)
}
/// Returns a copy of this view using `sep` as the separator between items.
///
/// The element types are preserved; only the separator type changes.
func withSeparator<S: View>(
@ViewBuilder _ sep: @escaping () -> S
) -> TupleView<First, repeat each Rest, S> {
.init(first, repeat each rest, separator: sep)
}
var body: some View {
ViewThatFits {
HStack(spacing: hspacing) {
// Render the pre‑built interleaved array
ForEach(Array(interleavedItems.enumerated()), id: \.offset) { index, view in
view
}
}
.frame(maxWidth: .infinity)
Menu(menuText.description) {
ForEach(Array(interleavedItems.enumerated()), id: \.offset) { index, view in
if index % 2 == 0 {
view
}
}
}
}
}
// MARK: - Compile‑time array builder (recursive with parameter packs)
private static func interleave<F: View, each R: View, S: View>(
first: F,
rest: repeat each R,
separator: @escaping () -> S
) -> [AnyView] {
var result = [AnyView(first)]
repeat result.append(contentsOf: [AnyView(separator()), AnyView(each rest)])
return result
}
}
/// 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)
}
}
}
}
#Preview {
TupleViewDemo()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment