Module 4 · Lesson 10 beginner

Form Controls — DatePicker, ColorPicker, ProgressView

NativeFirst Team 7 min read

We covered Toggle, Slider, and Stepper in lesson 6 when we were learning bindings. There are three more system controls that round out the form-input toolbox: DatePicker for time and dates, ColorPicker for colors, and ProgressView for “how far through this thing are we.” All three take bindings, all three look great by default, all three are easy to use wrong.

This lesson catalogs them, calls out the variant you’ll actually pick in production, and BrewLog gets a weekly goal progress bar plus a daily reminder time picker.


DatePicker — five styles, one decision tree

DatePicker binds to a Date and shows the system date/time picker. The two parameters that matter most are displayedComponents (which fields to expose) and datePickerStyle (how it looks).

DatePicker · five flavors
DatePicker styles: hourAndMinute, date, compact, ranged, graphical

The decision tree:

  • .hourAndMinute displayedComponent — for reminders, alarms, schedules. Picks just the time. (BrewLog uses this.)
  • .date displayedComponent — for birthdays, deadlines, anything calendar-shaped. Default style is a tappable label that opens a popover.
  • Default style + both components — for general datetime entry. Compact tap-to-expand UI.
  • in: range — bound the selection. Useful for “future only” or “past 30 days only.” The system disables out-of-range options instead of letting the user pick a bad value.
  • .graphical style — full visible monthly calendar. Use when picking is the main task on the screen, not a side input. Big, beautiful, feels like the system Calendar app.

The displayedComponents parameter gives you exact control over what’s editable. [.date] shows date only, [.hourAndMinute] shows time only, [.date, .hourAndMinute] (the default) shows both. Ask only for the components you need. A reminder time doesn’t need a date. A birthday doesn’t need a time.


ColorPicker — tiny API, big payoff

ColorPicker binds to a Color and opens the system color panel. Two-line component, infinite use cases.

ColorPicker examples
ColorPicker variants: with label, no opacity, hidden label

Three things to know:

  • supportsOpacity: false — restrict picking to opaque colors. Important when the color drives a fill or background where transparency would look broken.
  • .labelsHidden() — hide the built-in label so you can place a custom one (with weight, font, layout) outside the picker. The classic move when ColorPicker sits inside a card you’ve already styled.
  • Persistence is on you. Color doesn’t conform to Codable. To save a user-picked color in @AppStorage or SwiftData, store it as Data (using UIColor archiving) or as components (red, green, blue, opacity) and reconstruct on read. We’ll skip BrewLog’s accent-color picker for now to avoid that detour.

ColorPicker is one of those controls that’s perfect when you need it and pointless when you don’t. Don’t add a “theme color picker” to a settings screen unless users have actually asked for one.


ProgressView — linear, circular, determinate, indeterminate

ProgressView is the spinner-or-bar you reach for whenever something is loading or progressing. SwiftUI handles the look; you handle the value (or skip the value for “we don’t know how long this takes”).

ProgressView · four flavors
ProgressView variants: linear, total bounds, indeterminate, circular with value

The four shapes:

  • Determinate linearProgressView(value: progress) with value between 0 and 1. The most common shape. BrewLog uses this for the weekly goal.
  • Determinate with explicit totalProgressView(value: 7, total: 14). When the bounds aren’t 0…1, this avoids manual division.
  • Indeterminate — no value: parameter, just ProgressView("Loading…"). Animated pulse for unknown duration.
  • Circular.progressViewStyle(.circular). Use for compact spaces (status icons, inline indicators) or when the visual gauge metaphor is what you want.

.tint(.accentColor) on a ProgressView changes the fill color. .controlSize(.large) on a circular ProgressView makes a much larger gauge — useful for hero-area “loading” states.

The thing to never forget: if the operation can fail, don’t just show a ProgressView. Show a state machine — loading | loaded | empty | error. We’ll cover that pattern in Course 2 when we hit networking.


A trap every team hits: progress with no ceiling

The vibe-coded version of “show progress” almost always looks like this:

@State private var brewsLoggedToday = 0

ProgressView(value: Double(brewsLoggedToday))   // ← bug: out of bounds at brews ≥ 1

ProgressView(value:) expects 0…1. Pass 5 and the bar maxes out and silently stops working. The right fix is either an explicit total — ProgressView(value: Double(count), total: 14) — or normalizing to 0…1 yourself with a clamp:

var weeklyProgress: Double {
    guard weeklyGoal > 0 else { return 0 }
    return min(1.0, Double(weeklyCount) / Double(weeklyGoal))
}

That’s exactly what UserPreferences.weeklyProgress does. Always clamp derived progress values. A progress bar that quietly maxes out and stays still is one of those bugs nobody reports because nobody notices.


What it looks like running

BrewLog with weekly progress bar and daily reminder time picker
iPhone 17 Pro Max · iOS 26.2
BrewLog · weekly goal filling up

The video logs four brews — weekly progress fills 0/14 → 4/14 (28%). Then we scroll to Preferences and see the daily reminder time picker and the weekly goal stepper, both bound to the same UserPreferences model living in the environment.

This is also the first lesson where you can really see the data flow paying off: one model, eight controls reading and writing to it, every change reflected in two or three places without us writing a single update routine.


Takeaway

DatePicker for dates and times — match displayedComponents to what the user is actually picking, no more. ColorPicker when colors matter and persistence isn’t a problem. ProgressView for progress — clamp your values, model failure as a separate state.

Your form-control toolbox is now: Toggle, Slider, Stepper, DatePicker, ColorPicker, ProgressView, plus Button/Menu/Picker from last lesson. Nine controls, virtually every settings screen ever shipped.

Next lesson: lists. We finally turn “Coming next” into a real List of brews with sections, swipe actions, and the empty state every well-built iOS app handles properly.

N

NativeFirst Team

Editorial

The NativeFirst team — engineers and designers building native Apple apps and writing the courses we wish we had when we started.

Comments

Leave a comment

0/1000