Skip to content

Native Availability Picker ​

The local ChronoSketchCore and ChronoSketchUI packages provide civil availability rules, a retained model and SwiftUI multi-month selection. Availability Picker is included in the beta.17 source tag; the native demo is not an App Store release.

swift
import ChronoSketchCore
import ChronoSketchUI

// Throwing application setup on MainActor:
let start = try CivilDate("2026-09-14")
let data = try AvailabilityData(rules: [
  AvailabilityRule(
    range: AvailabilityRange(start: start, end: start.addingDays(120)),
    available: true, minimumDays: 3)
])
let model = try AvailabilityPickerModel(start: start, days: 120, data: data)

// SwiftUI body, retaining model outside repeated view construction:
AvailabilityPickerView(model: model) { selection in
  // Save a complete valid selection, or clear app state on nil.
}

See the input field reference and application flexibility for the shared rules and boundaries. Swift wraps start/end in AvailabilityRange with CivilDate values; AvailabilityData(rules:) validates the supplied intervals.

All ranges exclude their end. Rules separately specify available, canStart, canEnd, canTraverse (default true), minimumDays, and optional application reason. Nonoverlapping rule intervals are required; missing coverage is unknown. Minimum duration applies to all included dates. The last selectable date is start plus days minus one; full boundary months do not extend that limit. An unavailable but end-permitted day may close a selection without being part of its used interval.

setSelection updates selection without notification; explicit user reset invokes the callback with nil. setData replaces data and cancels pending loads. Active previews and enabled days immediately use the latest snapshot or provider reply. Direct completion calls still validate current data and explain conflicts. A completed selection remains visible if later data invalidates it.

setDataProvider supplies an application-owned Sendable async closure from AvailabilityRange to AvailabilityData. The view requests clipped visible-month windows. Return rules inside the query; uncovered dates remain unknown. Provider errors show retry; cancellation and generation checks suppress stale responses. reload refreshes, and suspendLoading cancels when hidden. Cache is temporary and bounded by the configured horizon. There is no networking or persistence client.

AvailabilityPickerConfiguration supplies locale, optional Sunday-zero week start, short weekday names (seven, Sunday first), month names (twelve, January first), visible-month count, layout column capacity and interactive/static mode. Haptics are on by default and can be disabled. showAvailable, showUnavailable, showRestrictions, showAnnotations and showPreview control indicators; showGaps optionally marks dates with a valid possible range.

text, dayContent and tooltipContent receive a date-aware context containing CivilDate, Sunday-zero weekday, ISO week/week-year, rule, annotation, selection and issue. Content closures return AnyView and remain inside reserved areas. Text and configuration are presentation values and can update without replacing the model. timeZone is optional callback context, not a date conversion instruction.

setAnnotations accepts nonoverlapping identified/tagged ranges with optional labels. These decorate dates without changing availability. SwiftUI tint/styles and custom content supply application appearance. Hourly selection is deferred. Physical haptics and a full VoiceOver audit need device acceptance beyond package and simulator checks.

Available-day shape ​

Web dayStyle: 'squared' | 'rounded' | 'flat' and Swift configuration.dayStyle (.squared / .rounded) control the available-day surface. Squared is the default. The surface is centered at 36px/pt maximum (Web shrinks it further in narrow cells), with small square corners or a circle. The month grid and minimum 44px/pt hit area stay unchanged. Restriction, annotation and bookable-gap indicators fit the same compact day surface. Selection and preview retain their range markings. Both demos expose this setting as “Day shape”.

Day badges ​

Set configuration.dayBadge to return an optional AvailabilityDayBadge for the existing date context. The badge sits at the upper-right edge of the 36pt surface, with a small overflow and no separate action. The day number and hit area remain unchanged; the localized label joins its accessibility description.

swift
configuration.dayBadge = { context in
  guard context.annotation != nil else { return nil }
  return AvailabilityDayBadge(
    color: .green, iconColor: .white, label: "Special offer",
    content: .icon("checkmark"))
}

Required fields are background color and localized label. iconColor defaults to white and also colors numbers. content is .dot by default (16pt), .icon(SFSymbolName) or .number(UInt) (16pt). Zero is visible; values above 99 show 99+, with the full count provided in label. Every variant is a fixed 16pt circle without a border. Returning nil removes the badge. Presentation changes preserve model selection. Badges are independent of rule/annotation data and showAnnotations; applications control their visibility. Annotation labels are metadata, no longer inline day text. Existing day/tooltip content closures remain supported.

Indicator color ​

indicatorColor (Swift: configuration.indicatorColor) sets period-annotation accents and restriction indicators on available days. Supply a CSS color on Web or SwiftUI Color on iOS; the default follows the accent color. For example, use indicatorColor: 'var(--xc-accent)' or configuration.indicatorColor = .accentColor. Bookable-gap outlines use a neutral border to distinguish them from the indicator. Unavailable-day styling, badge colors and selection colors remain independent. Changing this presentation option preserves selection and does not affect rules.

Badge precedence over annotation accents ​

A day with a badge suppresses its additional bottom annotation accent. This avoids a duplicate visual marker alongside the badge and bookable-gap outline. Removing the badge restores the annotation accent when annotations are enabled. Rule restriction indicators, gap outlines and selection markings remain independent. This presentation precedence applies on Web and iOS and changes no availability or annotation data.

Badge sizing refinement: dot, icon and number share a fixed 16px/pt circle. No border, outline or outline-like shadow is drawn. Numeric content uses 7px/pt text so 99+ fits without changing badge dimensions.

Selection shape refinement: squared day styling uses a small 4px/pt selection corner radius. Web start/end round only the outer corners and retain straight inner connections. Rounded mode retains its existing selection geometry. Switching shape preserves the selected dates and does not affect selection rules.

Shared picker frame — 2026-09-15 ​

All visible months belong to one shared card with common navigation and selection feedback/actions. Month headings use a subtle surface band and separator; months are separated by fine rules, not individually elevated cards. Side-by-side months have vertical separation; stacked rows have horizontal separation.

The card follows the configured column capacity (up to 360px/pt per column, capped by visible-month count and available width). Narrow layouts retain all months and stack them. Web removes the unused reserved detail height; actual feedback retains its accessible live region. There is no new model option or change to selection, loading, badges, date semantics or static-mode rules. Existing shape settings continue to control days and selection endpoints independently of the outer card.

Unavailable days use one thin, closed contour in the muted/secondary foreground color, matching the 36px/pt day shape. It replaces the restriction underline for these days, rather than adding a second border. The strikethrough remains; showUnavailable controls both marks. Available-day indicators are unchanged.

Shared hint card ​

showHints defaults to true. Interactive pickers show one bottom hint card with a leading accent indicator, shared across all months. Pointer/focus feedback on Web and the current touched/hovered day on iOS update this card. Disable it independently of selection previews; static mode has no hint card. Web uses a sticky footer bounded by the picker; native uses a bottom safe-area inset. The card grows with its content and reserves no empty lines for possible day details. Native hides the card when neither a selection nor a day hint is present. text and existing custom tooltip content supply its contents. Selection errors and loading feedback remain visible independently of this setting. Both demo settings expose Hint card.

This replaces the former per-month Web detail area. No data or selection contract changes.

The confirmed selection appears inside the hint card above the changing day hint. Hover/focus updates never replace that selection summary. With hints disabled, the summary remains in the ordinary footer/feedback area. The Web demo no longer duplicates raw ISO selection dates below the picker.

Native selection haptics emit one signal after validation: a soft activation for an accepted start, success for a valid completed range, and error for a rejected direct selection call (including changed availability). Disabled UI buttons emit neither selection callbacks nor haptics. No activation pulse precedes success/error. hapticsEnabled = false suppresses all three. Success confirms the local date selection, not a booking or persistence in the application.

Month cards use independent cardStyle (rounded, squared, flat) with 12px/pt gaps. Day styles only affect day markers and selection boundaries. Frame style controls the outer surface. See the styling contract.

Native selection follows the Web range treatment: full-cell connected bands, accent-filled start/end and a lighter interior. Only the start's leading edge and the end's trailing edge are rounded (24pt rounded, 4pt squared); interior edges are straight. Week/month wraps keep their grid boundaries without adding false start/end caps. An active start is accent-filled before completion. The briefly introduced separate native selection circles are superseded.

The bottom hint card shares the picker card width limit and 12pt outer inset, remaining centered and edge-aligned on wide displays.

Boundary-only dates and selection ​

Without setSelection, the model has no selected range or start anchor. Set an explicit AvailabilitySelection before displaying the view to preselect a range. After choosing a start, only valid end buttons are enabled. Invalid buttons are visually dimmed and expose native disabled semantics plus a localized reason to accessibility. Selected dates retain their selected trait and highlight. Reset clears the anchor and selection, emits nil and permits a fresh start, even if an update leaves no valid endpoint. These behaviors do not depend on optional visual indicators or hover.

A partially occupied date may permit boundary use while forbidding traversal:

swift
let boundary = try CivilDate("2026-09-29")
let partialDay = try AvailabilityRule(
  range: AvailabilityRange(start: boundary, end: boundary.addingDays(1)),
  available: true, canTraverse: false)

With otherwise free data, a September 28 start can end September 29, but cannot end September 30. September 29 may also start a range. Availability, canStart, canEnd and included-day minimum duration still apply. Rules spanning multiple dates prohibit each strictly interior date, including dates following the start within that same rule. Exact boundary times and final reservation checks remain consumer responsibilities. Clipped provider rules retain canTraverse.

The native demo's “Partially occupied boundary day” setting uses tomorrow as this boundary and updates the existing model immediately; switching it off restores the seasonal example without clearing selection.

Reset placement ​

Available in beta.17: AvailabilityPickerConfiguration.resetPlacement uses the public AvailabilityResetPlacement enum (.header, .footer). The default is .footer, matching Web's resetPlacement: 'footer'.

swift
var configuration = AvailabilityPickerConfiguration()
configuration.resetPlacement = .header
AvailabilityPickerView(model: model, configuration: configuration) { selection in
  // nil clears consumer state after either a partial or complete selection.
}

In interactive mode the reset Button remains present, disabled when the model has neither an anchor nor a selection. Choosing the first start enables it. Reset clears anchor, selection and preview and calls the existing callback with nil. Header order is title, reset, previous month, next month; narrow layouts move the title to a separate line. Native Button disabled semantics expose the state to accessibility. The footer has no reset or reserved reset space in header mode. Static mode has no controls. Configuration changes retain the model's selection. Both demos expose the placement setting, using the existing localized labels and shared button styles. Physical-device VoiceOver acceptance remains outstanding.

ChronoSketch · Calendar and scheduling components