Weekday Segmented Picker
A row of round day buttons that picks several days of the week in the calendar's order, with a summary like Weekdays under it.

- Controls
- iOS 18.0+
- beginner
- Origin
- Original work
- Found by
- weekday picker · days of the week · multi-select · segmented · alarm · repeat · schedule · recurrence · selector de días · días de la semana · selección múltiple · segmentado · alarma · repetir · horario · recurrencia
Description
Section titled “Description”A row of round day buttons that picks several days of the week, like the repeat days of an alarm, with a summary such as "Weekdays" under it. The days follow the calendar: its first weekday and its own letters.
The selection holds Calendar weekday numbers, 1 for Sunday. With Reduce Motion on, a day changes color without popping.
struct WeekdaySegmentedPickerDemo: View { @State private var alarmDays: Set<Int> = [2, 4] @State private var gymDays: Set<Int> = [3, 5, 7]
var body: some View { VStack(alignment: .leading, spacing: 20) { Text("Repeat") .font(.largeTitle.bold())
VStack(alignment: .leading, spacing: 14) { Label("7:30 Wake Up", systemImage: "alarm.fill") .font(.headline) WeekdaySegmentedPicker(selection: $alarmDays, color: .orange) } .padding(18) .background(Color(uiColor: .secondarySystemGroupedBackground), in: .rect(cornerRadius: 22, style: .continuous))
VStack(alignment: .leading, spacing: 14) { Label("Workout", systemImage: "figure.run") .font(.headline) WeekdaySegmentedPicker(selection: $gymDays, color: .mint) } .padding(18) .background(Color(uiColor: .secondarySystemGroupedBackground), in: .rect(cornerRadius: 22, style: .continuous)) } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
selection | Binding<Set<Int>> | Required |
calendar | Calendar | .current |
color | Color | .accentColor |
showsSummary | Bool | true |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
struct WeekdaySegmentedPickerDay: Identifiable, Equatable { /// The `Calendar` weekday number: 1 is Sunday and 7 is Saturday. let weekday: Int let letter: String let name: String let shortName: String
var id: Int { weekday }}
enum WeekdaySegmentedPickerSummary: Equatable { case never, everyDay, weekdays, weekends case days([Int])}
enum WeekdaySegmentedPickerCalendar { /// The seven days in the order of the calendar, starting at its first weekday. static func days(for calendar: Calendar) -> [WeekdaySegmentedPickerDay] { let letters = calendar.veryShortStandaloneWeekdaySymbols let names = calendar.standaloneWeekdaySymbols let shortNames = calendar.shortStandaloneWeekdaySymbols
return (0..<7).map { offset in let index = (calendar.firstWeekday - 1 + offset) % 7 return WeekdaySegmentedPickerDay(weekday: index + 1, letter: letters[index], name: names[index], shortName: shortNames[index]) } }
/// Names a selection the way an alarm does. Saturday and Sunday make the weekend. static func summary(of selection: Set<Int>, order: [Int]) -> WeekdaySegmentedPickerSummary { let chosen = order.filter(selection.contains) switch Set(chosen) { case []: return .never case Set(1...7): return .everyDay case Set(2...6): return .weekdays case [1, 7]: return .weekends default: return .days(chosen) } }}
/// A row of round day buttons that picks several days of the week, like the/// repeat days of an alarm, with a summary such as "Weekdays" under it./// The days follow the calendar: its first weekday and its own letters.////// The selection holds `Calendar` weekday numbers, 1 for Sunday. With Reduce/// Motion on, a day changes color without popping.struct WeekdaySegmentedPicker: View { @Binding var selection: Set<Int> var calendar: Calendar = .current var color: Color = .accentColor var showsSummary = true
@ScaledMetric(relativeTo: .body) private var size = 40.0 @Environment(\.accessibilityReduceMotion) private var reduceMotion
private var days: [WeekdaySegmentedPickerDay] { WeekdaySegmentedPickerCalendar.days(for: calendar) }
var body: some View { VStack(spacing: 12) { HStack(spacing: 0) { ForEach(days) { day in dayButton(day) } }
if showsSummary { summaryText .font(.subheadline) .foregroundStyle(.secondary) .contentTransition(.opacity) .animation(.easeInOut(duration: 0.2), value: selection) } } .sensoryFeedback(.selection, trigger: selection) }
private func dayButton(_ day: WeekdaySegmentedPickerDay) -> some View { let isSelected = selection.contains(day.weekday)
return Button { if isSelected { selection.remove(day.weekday) } else { selection.insert(day.weekday) } } label: { Text(day.letter) .font(.subheadline.weight(.semibold)) .minimumScaleFactor(0.6) .foregroundStyle(isSelected ? AnyShapeStyle(WeekdaySegmentedPickerContrastingForeground(background: color)) : AnyShapeStyle(.primary)) .frame(maxWidth: size, maxHeight: size) .aspectRatio(1, contentMode: .fit) .background(isSelected ? AnyShapeStyle(color) : AnyShapeStyle(.quaternary), in: .circle) .scaleEffect(isSelected && !reduceMotion ? 1.06 : 1) .frame(maxWidth: .infinity, minHeight: 44) .contentShape(.rect) } .buttonStyle(.plain) .animation(reduceMotion ? .easeInOut(duration: 0.15) : .spring(duration: 0.3, bounce: 0.5), value: isSelected) .accessibilityLabel(day.name) .accessibilityAddTraits(isSelected ? .isSelected : []) }
private var summaryText: Text { switch WeekdaySegmentedPickerCalendar.summary(of: selection, order: days.map(\.weekday)) { case .never: Text("Never") case .everyDay: Text("Every day") case .weekdays: Text("Weekdays") case .weekends: Text("Weekends") case .days(let weekdays): Text(days.filter { weekdays.contains($0.weekday) }.map(\.shortName).formatted(.list(type: .and))) } }}
nonisolated struct WeekdaySegmentedPickerContrastingForeground: ShapeStyle { let background: Color
func resolve(in environment: EnvironmentValues) -> Color { let color = background.resolve(in: environment) let luminance = 0.2126 * color.linearRed + 0.7152 * color.linearGreen + 0.0722 * color.linearBlue return luminance > 0.5 ? .black : .white }}
// MARK: - Demo
struct WeekdaySegmentedPickerDemo: View { @State private var alarmDays: Set<Int> = [2, 4] @State private var gymDays: Set<Int> = [3, 5, 7]
var body: some View { VStack(alignment: .leading, spacing: 20) { Text("Repeat") .font(.largeTitle.bold())
VStack(alignment: .leading, spacing: 14) { Label("7:30 Wake Up", systemImage: "alarm.fill") .font(.headline) WeekdaySegmentedPicker(selection: $alarmDays, color: .orange) } .padding(18) .background(Color(uiColor: .secondarySystemGroupedBackground), in: .rect(cornerRadius: 22, style: .continuous))
VStack(alignment: .leading, spacing: 14) { Label("Workout", systemImage: "figure.run") .font(.headline) WeekdaySegmentedPicker(selection: $gymDays, color: .mint) } .padding(18) .background(Color(uiColor: .secondarySystemGroupedBackground), in: .rect(cornerRadius: 22, style: .continuous)) } .padding(20) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .background(Color(uiColor: .systemGroupedBackground).ignoresSafeArea()) }}
#Preview { WeekdaySegmentedPickerDemo()}