Skip to content

Instantly share code, notes, and snippets.

@valvoline
Created August 25, 2025 16:59
Show Gist options
  • Save valvoline/82363ca039749373687e134afdcd5654 to your computer and use it in GitHub Desktop.
Save valvoline/82363ca039749373687e134afdcd5654 to your computer and use it in GitHub Desktop.
A simple custom segmented control written in SwiftUI
import SwiftUI
struct SegmentedItem: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
var description: String {
name.capitalized
}
init(_ name: String) { self.name = name }
}
struct ContentView: View {
@Namespace private var animation
@Namespace private var animation2
@State private var idAnimation: String = ""
let options = [
SegmentedItem("house"),
SegmentedItem("trophy"),
SegmentedItem("person"),
SegmentedItem("gear")
]
var body: some View {
VStack {
HStack {
VStack {
ForEach(options) { option in
Text(option.description)
.padding(.vertical, 10)
.matchedGeometryEffect(id: option.name, in: animation2)
}
}
GeometryReader { geo in
let width = geo.size.width
let height = geo.size.height
let origin = geo.frame(in: .local).origin
Image(systemName: "chevron.left")
.resizable()
.frame(width: 8, height: 11)
.offset(x: width + origin.x + 4, y: (height-11)/2)
}
.foregroundStyle(.white)
.matchedGeometryEffect(id: idAnimation, in: animation2, properties: [.frame, .position, .size], isSource: false)
}
.fixedSize(horizontal: true, vertical: true)
.padding(.bottom, 40)
HStack(spacing: 48) {
ForEach(options) { option in
Button {
withAnimation {
idAnimation = option.name
}
} label: {
VStack {
Image(systemName: option.name)
.resizable()
.frame(width: 24, height: 24)
Text(option.description).font(.caption2)
}
.frame(maxWidth: .infinity)
}
.foregroundStyle(idAnimation == option.name ? .white : .black)
.matchedGeometryEffect(id: option.name, in: animation)
}
}
.padding(.horizontal, 2)
.background {
RoundedRectangle(cornerRadius: 14)
.fill(.ultraThinMaterial)
.scaleEffect(1.4)
.matchedGeometryEffect(id: idAnimation, in: animation, isSource: false)
}
.padding()
.glassEffect(.clear.interactive(), in: RoundedRectangle(cornerRadius: 24))
.onAppear {
idAnimation = options.first?.name ?? ""
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.padding()
.background {
TimelineView(.animation) { context in
let t = context.date.timeIntervalSinceReferenceDate
let angle = Angle(degrees: (t * 30)
.truncatingRemainder(dividingBy: 360))
MeshGradient(
width: 2,
height: 2,
points: [
.init(x: 0.0, y: 0.0),
.init(x: 1.0, y: 0.0),
.init(x: 0.0, y: 1.0),
.init(x: 1.0, y: 1.0)
],
colors: [
.red, .blue,
.green, .purple
]
)
.hueRotation(angle)
.ignoresSafeArea()
}
}
}
}
#Preview {
ContentView()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment