Skip to content

Instantly share code, notes, and snippets.

@mbernson
Last active July 14, 2023 17:04
Show Gist options
  • Save mbernson/ff5b18f60f01d759c27ceddd3385427c to your computer and use it in GitHub Desktop.
Save mbernson/ff5b18f60f01d759c27ceddd3385427c to your computer and use it in GitHub Desktop.
SwiftUI custom segmented control
import SwiftUI
struct ContentView: View {
@State var selected = "Foo"
var body: some View {
SegmentedControl(options: ["Foo", "Bar", "Baz"], selectedOption: $selected) { label in
Text(label)
}
}
}
//
// SegmentedControl.swift
//
//
// Created by Mathijs Bernson on 10/01/2023.
// Copyright © 2023 Q42. All rights reserved.
//
import SwiftUI
struct SegmentedControl<Label, SelectionValue>: View where Label: View, SelectionValue: Hashable {
let options: [SelectionValue]
@Binding var selectedOption: SelectionValue
@ViewBuilder var label: (SelectionValue) -> Label
@Namespace private var animation
@ScaledMetric private var height: CGFloat = 44
/// Creates a segmented control that displays a custom view for its segment labels.
init(options: [SelectionValue], selectedOption: Binding<SelectionValue>, label: @escaping (SelectionValue) -> Label) {
self.options = options
self._selectedOption = selectedOption
self.label = label
}
/// Creates a segmented control that displays a string for its segment labels.
init(options: [SelectionValue], selectedOption: Binding<SelectionValue>, label: @escaping (SelectionValue) -> String) where Label == Text {
self.options = options
self._selectedOption = selectedOption
self.label = { value in
Text(label(value))
}
}
/// Creates a segmented control that displays a string for its segment labels.
init(options: [SelectionValue], selectedOption: Binding<SelectionValue>) where Label == Text, SelectionValue == String {
self.options = options
self._selectedOption = selectedOption
self.label = { value in
Text(value)
}
}
var body: some View {
HStack {
ForEach(options, id: \.self) { option in
let isSelected = (selectedOption == option)
Button {
withAnimation(.easeOut(duration: 0.2)) {
selectedOption = option
}
} label: {
label(option)
}
.frame(height: height)
.buttonStyle(SegmentedButtonStyle(isSelected: isSelected))
.background {
if isSelected {
RoundedRectangle(cornerRadius: 15, style: .continuous)
.fill(.white)
.padding(2)
.matchedGeometryEffect(id: "Shape", in: animation)
}
}
}
}
.background(.thinMaterial, in: RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
private struct SegmentedButtonStyle: ButtonStyle {
let isSelected: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.foregroundColor(isSelected ? .black : .white)
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
}
}
struct SegmentedControl_Previews: PreviewProvider {
@State static var selected = "Foo"
static var previews: some View {
SegmentedControl(options: ["Foo", "Bar", "Baz"], selectedOption: $selected) { label in
Text(label)
}
.padding()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment