swiftcnearly preview
Component library

Progress

Native progress composition, package imports, and editable Swift source.

Preview

Progress rendered by native SwiftUI.Progress rendered by native SwiftUI. Dark appearance.
Captured from the native SwiftUI catalog on macOS.

Use it

Import SwiftCN to use CNProgress directly. This complete example also runs in the native component gallery.

ProgressExample.swift
import SwiftUI
import SwiftCN

struct ProgressExample: View {

    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            CNProgress("Uploading", value: 0.65)

        }
    }
}

Native behavior

A native ProgressView supports determinate values or an indeterminate nil value. Finite positive totals define the range; values are clamped. Determinate changes use feedback-motion and respect Reduce Motion.

Style and compose

Visual recipes use .tw classes. Append classes: to override a recipe locally, or change the recipe in TWGlobalRules.named for your subtree. Native layout and interaction modifiers remain SwiftUI APIs. For control colors, append cn-tint-[primary], another theme token, or an interpolated Color.

Motion recipes are editable too: control-motion uses a 150 ms smooth response, disclosure-motion uses a 250 ms spring, and feedback-motion uses a 200 ms smooth response. Set a recipe to animate-none in your global rules to turn it off. Presets respect Reduce Motion. Static components stay still; native scrolling, drag tracking, and platform presentation keep their own gestures.

Action groups and card, dialog, and questionnaire footers stack when their content does not fit. The native layout preserves the same child controls during this change.

View builders preserve native child content. Native Button roles, labels, keyboard shortcuts, submit actions, and presentation modifiers still work. A string recipe does not replace a native control API or a typed chart mark.

Own the source

Copy the complete file below into your app alongside import SwiftCN. The file contains related composable parts from the same family. Its dependencies use public package APIs, so copied and imported components can coexist.

To own the styling engine too, download the complete source bundle. It contains every component, the .tw engine, its recipes, and the MIT license. Keep those files in one module and remove import SwiftCN from the usage example.

ValueControls.swift
import SwiftUI
import SwiftCN

public struct CNSlider: View {
    private let title: LocalizedStringKey
    @Binding private var value: Double
    private let range: ClosedRange<Double>
    private let step: Double
    private let classes: TWClasses
    private let onEditingChanged: (Bool) -> Void
    public init(_ title: LocalizedStringKey, value: Binding<Double>, in range: ClosedRange<Double> = 0...1,
                step: Double = 0.01, classes: TWClasses = "", onEditingChanged: @escaping (Bool) -> Void = { _ in }) {
        precondition(range.lowerBound.isFinite && range.upperBound.isFinite && step.isFinite && step > 0)
        self.title = title; _value = value; self.range = range; self.step = step
        self.classes = classes; self.onEditingChanged = onEditingChanged
    }
    public var body: some View {
        Slider(value: $value, in: range, step: step, onEditingChanged: onEditingChanged) { Text(title) }
            .tw(cn("slider", classes)).cnControlUtilities()
    }
}
public struct CNProgress: View {
    private let title: LocalizedStringKey
    private let value: Double?
    private let total: Double
    private let classes: TWClasses
    public init(_ title: LocalizedStringKey, value: Double? = nil, total: Double = 1, classes: TWClasses = "") {
        precondition(total.isFinite && total > 0)
        self.title = title; self.value = value.map { $0.isFinite ? min(total, max(0, $0)) : 0 }
        self.total = total; self.classes = classes
    }
    public var body: some View {
        ProgressView(title, value: value, total: total).tw(cn("progress feedback-motion", classes), value: value, animationScope: .content).cnControlUtilities()
    }
}
public struct CNDatePicker: View {
    @Environment(\.calendar) private var calendar
    @Environment(\.locale) private var locale
    @Environment(\.timeZone) private var timeZone
    @FocusState private var fieldFocused: Bool
    @FocusState private var triggerFocused: Bool
    @State private var calendarPresented = false
    private let title: LocalizedStringKey
    @Binding private var date: Date
    private let range: ClosedRange<Date>
    private let components: DatePickerComponents
    private let classes: TWClasses
    public init(_ title: LocalizedStringKey, selection: Binding<Date>,
                in range: ClosedRange<Date> = Date.distantPast...Date.distantFuture,
                displayedComponents: DatePickerComponents = .date, classes: TWClasses = "") {
        self.title = title; _date = selection; self.range = range; components = displayedComponents; self.classes = classes
    }
    private var format: Date.FormatStyle {
        Date.FormatStyle(date: .numeric, time: components.contains(.hourAndMinute) ? .shortened : .omitted,
                         locale: locale, calendar: calendar, timeZone: timeZone)
    }
    private var selection: Binding<Date> {
        Binding(get: { date }, set: { proposed in
            var localCalendar = calendar; localCalendar.timeZone = timeZone
            date = CNDatePickerSelection.resolve(proposed, preservingTimeFrom: date,
                preserveTime: !components.contains(.hourAndMinute), in: range, calendar: localCalendar)
        })
    }
    private var calendarSelection: Binding<Date> {
        Binding(get: { date }, set: { selected in
            var localCalendar = calendar; localCalendar.timeZone = timeZone
            date = CNDatePickerSelection.resolve(selected, preservingTimeFrom: date,
                preserveTime: true, in: range, calendar: localCalendar)
            if !components.contains(.hourAndMinute) { calendarPresented = false }
        })
    }
    private var field: some View {
        TextField(title, value: selection, format: format).textFieldStyle(.plain)
            .focused($fieldFocused).tw("input-group-field")
            .accessibilityLabel(Text(title))
    }
    private var calendarButton: some View {
        Button { fieldFocused = false; calendarPresented.toggle() } label: {
            Image(systemName: "calendar").accessibilityHidden(true)
        }
        .focused($triggerFocused)
        // An input adornment, not a standalone button: it keeps a plain button inside the field.
        .buttonStyle(.tw("px-2 py-0 text-mutedForeground hover:text-foreground"))
        .accessibilityLabel("Choose date")
        .accessibilityValue(calendarPresented ? Text("Expanded") : Text("Collapsed"))
    }
    private var calendarContent: some View {
        VStack(spacing: 8) {
            CNCalendar(title, selection: calendarSelection, in: range,
                       classes: "date-picker-calendar", dayClasses: calendarDayClasses)
            if components.contains(.hourAndMinute) {
                DatePicker("Time", selection: $date, in: range, displayedComponents: .hourAndMinute)
                    .datePickerStyle(.compact).tw("px-3")
                CNButton("Done", variant: .ghost, action: { calendarPresented = false })
            }
        }.tw("popover p-1")
    }
    @ViewBuilder public var body: some View {
        if components.contains(.date) {
            VStack(alignment: .leading, spacing: 8) {
                CNLabel { Text(title) }
                CNInputGroup(focus: $fieldFocused) {
                    field
                    calendarButton
                }
                .cnPopover(isPresented: $calendarPresented, alignment: .center) { calendarContent }
            }.tw(cn("date-picker", classes)).cnControlUtilities()
                .onChange(of: calendarPresented) { _, open in
                    if open { fieldFocused = false }
                    else { triggerFocused = true }
                }
        } else {
            DatePicker(title, selection: $date, in: range, displayedComponents: components)
                .datePickerStyle(.compact).tw(cn("date-picker", classes)).cnControlUtilities()
        }
    }
    private var calendarDayClasses: TWClasses {
        #if os(iOS)
        ""
        #else
        "h-[32]"
        #endif
    }
}

enum CNDatePickerSelection {
    static func resolve(_ proposed: Date, preservingTimeFrom current: Date, preserveTime: Bool,
                        in range: ClosedRange<Date>, calendar: Calendar) -> Date {
        let resolved: Date
        if preserveTime {
            let time = calendar.dateComponents([.hour, .minute, .second], from: current)
            resolved = calendar.date(bySettingHour: time.hour ?? 0, minute: time.minute ?? 0,
                                     second: time.second ?? 0, of: proposed) ?? proposed
        } else {
            resolved = proposed
        }
        return min(max(resolved, range.lowerBound), range.upperBound)
    }
}

On this page