Yotei1
A highly modular, highly customizable calendar package for iOS. Built with SwiftUI and UIKit under the hood for the best performance and native feel.
Every component can be used on its own or composed into a full calendar app. Pick only what you need — a date picker, a schedule list, a day timeline, an all-day grid — or wire them all together.
- Features
- Why Yotei
- Requirements
- Installation
- Quick Start
- Available Components
- Typed Event Data
- Colors Customization
- Fonts Customization
- Customization with View Factories
- Handling User Interaction
- Example App
- Roadmap
- License
- Composable by design. Each view is an independent SwiftUI
View— use one, use several, arrange them however your layout requires. - SwiftUI API, UIKit performance. Heavy surfaces (scrolling schedule list, paging strip, date tabs) are backed by
UICollectionViewandUIPageViewControllerfor smooth scrolling even with thousands of events. - Deep customization via view factories. Every cell, header, button, marker, and layout metric is produced by a protocol you can implement — no subclassing, no private API, no fighting the framework.
- Calendar-aware. Respects the
\.calendarenvironment, custom time zones, first-weekday settings, and locale-driven symbols. - Drop-in defaults, escape hatches everywhere. Start with
YoteiScheduleView(...)and ship in two lines. Need a branded event pill? Implement one factory method. Need a fully custom day cell? Implement another. You are never locked in. - Modern Swift. Swift 6.2, strict concurrency,
@MainActor-correct factories,Sendabledomain types. - Production ready. Support for iOS 16+ makes Yotei available not only for modern startups but even for mature projects.
Most open-source iOS calendar libraries fall into one of two camps:
- "Monolithic" components — one giant view that owns layout, data, theming, and gestures. Great for demos, painful once the design system pushes back.
- "Bring your own everything" — low-level primitives that still require you to write the scrolling list, the page controller, and the event layout from scratch.
If you want a date picker today and a full-screen planner tomorrow without switching libraries, Yotei is built for that path.
- iOS 16+
- Swift 6.2+
- Xcode 16+
Add Yotei to your Package.swift:
dependencies: [
.package(url: "https://github.com/claustrofob/Yotei.git", branch: "main"),
]Then add it to your target:
.target(
name: "YourApp",
dependencies: ["Yotei"]
)Or in Xcode: File → Add Package Dependencies… and enter https://github.com/claustrofob/Yotei.git.
Then import where needed:
import YoteiA minimal agenda-style calendar with the built-in strip, weekday titles, and a scrolling schedule list:
import SwiftUI
import Yotei
struct CalendarScreen: View {
@State private var focusedDate = Date()
@State private var data = YoteiEventsInterval()
var body: some View {
VStack(spacing: 0) {
YoteiWeekdayTitlesView()
YoteiStripContainerView(focusedDate: $focusedDate)
YoteiScheduleView(
focusedDate: $focusedDate,
data: $data
)
}
.onChange(of: focusedDate) { _ in
// Load events for the visible month and assign them to `data.events`
}
}
}A standalone date picker with a min/max range:
import SwiftUI
import Yotei
struct PickerScreen: View {
@State private var selectedDate = Date()
var body: some View {
YoteiDatePicker(
selectedDate: $selectedDate,
minDate: Calendar.current.date(byAdding: .day, value: -1, to: Date()),
maxDate: Calendar.current.date(byAdding: .month, value: 2, to: Date())
)
.padding()
}
}A full day view with an all-day header and a scrollable hour timeline:
YoteiPagesDayView(focusedDate: $focusedDate) { date in
VStack(spacing: 0) {
YoteiAllDayEventsTopView(
startDate: date,
numberOfDays: 1,
data: $data
)
YoteiDayEventsView(
dayDate: date,
numberOfDays: 1,
data: $data,
contentOffset: $contentOffset
)
}
}A full month grid with paging between months and multi-day event bars:
VStack(spacing: 0) {
YoteiWeekdayTitlesView()
YoteiPagesMonthView(focusedDate: $focusedDate) { date in
YoteiPagesMonthPageView(
selectedDate: $focusedDate,
data: $data,
dateInMonth: date
)
}
}All examples use only default configuration — no factories, no subclasses.
Every view below is a public SwiftUI View that lives under import Yotei.
| Component | Purpose |
|---|---|
YoteiDatePicker |
Full month calendar date picker with month/year selector, paging between months, and optional minDate / maxDate. |
YoteiDatePickerMonth |
A single month grid — useful when you want to embed one month into a custom layout. |
YoteiMonthYearPicker |
Wheel-style month/year selector used by YoteiDatePicker when expanded. |
YoteiTimePicker |
Wheel-style time picker (24-hour) with configurable minute interval. |
| Component | Purpose |
|---|---|
YoteiStripContainerView |
Collapsible week ↔ month strip with drag-to-expand, paging, and a tap-to-collapse expand button. |
YoteiStripWeekView / YoteiStripMonthView |
Individual week and month strips exposed for custom containers. |
YoteiWeekdayTitlesView |
Localized short weekday titles ("M T W T F S S"), aware of first weekday and locale. |
YoteiWeekdaysView |
Row of date cells for a given week start — handy when assembling custom headers. |
| Component | Purpose |
|---|---|
YoteiPagesDayView |
Infinite horizontal pager, one page per day. Bound to a focused Date. |
YoteiPagesWeekView |
Infinite horizontal pager, one page per week, always aligned to the calendar's first weekday. |
YoteiPagesMonthView |
Infinite horizontal pager, one page per month. Bound to a focused Date. |
| Component | Purpose |
|---|---|
YoteiScheduleView |
Scrolling agenda list grouped by day. UIKit-backed for smooth scroll over large ranges. |
YoteiDayEventsView |
Hour-by-hour day timeline with overlap layout, current-time marker, and tap-to-create gesture. Supports multi-day layouts (numberOfDays: 7 for a week view). |
YoteiAllDayEventsTopView |
Multi-column grid for all-day and multi-day events, with a "+N more" indicator. |
YoteiPagesMonthPageView |
A single month grid with a 6-row week layout, multi-day event bars, and a "+N" overflow indicator. Designed to be embedded inside YoteiPagesMonthView. |
| Type | Purpose |
|---|---|
YoteiEvent<Data> |
Immutable event model: id, title, start, end, isAllDay, and a generic data payload. Timezone-safe display helpers included. |
YoteiEventsInterval |
The data envelope every events view reads from: visible interval, month interval, loading interval, and the [Date: [YoteiEvent]] bucket. |
YoteiDelegate |
Single delegate protocol for event taps, all-day slot taps, and time-slot selection (tap-to-create). |
YoteiDaysSequence |
A lazy, random-access Collection of Dates — useful when you iterate your own days. |
YoteiEvent is generic over a Data payload so you can carry your own domain model alongside the calendar fields without subclassing, wrapping, or type-erasing:
public struct YoteiEvent<Data: YoteiEventData>: Equatable, Identifiable, Sendable {
public let id: String
public let title: String
public let start: Date
public let end: Date
public let isAllDay: Bool
public let data: Data
}
public typealias YoteiEventData = Equatable & SendableData is whatever you need — a color, a list of attendees, a remote ID, a source enum, a full DTO from your backend. The only requirement is that it is Equatable and Sendable.
nonisolated struct EventPayload: Equatable, Sendable {
let calendarID: String
let tint: Color
let attendees: [String]
let isReadOnly: Bool
}
let event = YoteiEvent(
id: "evt-42",
title: "Design review",
start: start,
end: end,
isAllDay: false,
data: EventPayload(
calendarID: "work",
tint: .indigo,
attendees: ["alex", "sam"],
isReadOnly: false
)
)The generic parameter propagates through the whole pipeline so your payload is available at every extension point, fully typed.
struct TintedDayEventsFactory: YoteiDayEventsViewFactoryProtocol {
func eventView(event: YoteiEvent<EventPayload>) -> some View {
YoteiDayEventsViewFactory()
.eventView(event: event)
.tint(event.data.tint)
.opacity(event.data.isReadOnly ? 0.6 : 1.0)
}
}No casting, no userInfo: [String: Any], no lookups into a sidecar dictionary keyed by event.id.
Use an empty marker struct:
nonisolated struct EventData: Equatable, Sendable {}You pay nothing for the generic — the payload is a zero-sized field — and you can introduce real data later without rewriting any call sites.
Every default view uses standard SwiftUI shape styles — .tint, .background, .primary, .secondary, .tertiary — so you can re-color the calendar with the standart SwiftUI modifiers:
.foregroundStyle(_:_:_:)- redefine .primary, .secondary and .tertiary styles.backgroundStyle(_:)- redefine .background style.tint(_:)- redefine .tint style
You have a few options to set custiom colors in calendar:
- apply the above modifiers globally on calendar component
- aply them on individual default views in custom view factories
- use your custom views with custom colors in view factories.
VStack(spacing: 0) {
YoteiWeekdayTitlesView()
YoteiStripContainerView(focusedDate: $focusedDate)
YoteiScheduleView(focusedDate: $focusedDate, data: $data)
}
.tint(.purple)Because .tint is a normal SwiftUI environment value, you can scope it to one component too — tint only the strip, only the schedule, only one page:
YoteiStripContainerView(focusedDate: $focusedDate)
.tint(.indigo)
YoteiScheduleView(focusedDate: $focusedDate, data: $data)
.tint(.orange)Inside a view factory you can apply .tint on a per-event basis using the typed event.data payload — the default event view fills with .tint, so changing the tint changes the pill color:
struct BrandedDayEventsFactory: YoteiDayEventsViewFactoryProtocol {
func eventView(event: YoteiEvent<EventPayload>) -> some View {
YoteiDayEventsViewFactory()
.eventView(event: event)
.tint(event.data.tint)
}
}Default cells render text with .primary / .secondary / .tertiary and surfaces with .background, so they automatically follow the system's light/dark appearance and any .preferredColorScheme(_:) you set. To diverge from the system palette, wrap the default factory output and apply .foregroundStyle(_:) or .background(_:) on top — there is no need to re-implement the cell.
For anything finer-grained — borders, gradients, conditional colors per state — drop into a view factory and override only the method you need.
Every default view renders text using a small, shared set of font roles exposed via YoteiFontStyle.
The active style lives in the SwiftUI environment under \.yoteiFontStyle, so you can override it globally on a calendar component, scope it to an individual view, or apply it inside a view factory.
You have a few options to set custom fonts in calendar:
- inject a custom
YoteiFontStyleglobally on calendar component via the\.yoteiFontStyleenvironment key - inject it on individual default views in custom view factories
- use your custom views with custom fonts in view factories.
Apply a branded font style to the whole calendar:
VStack(spacing: 0) {
YoteiWeekdayTitlesView()
YoteiStripContainerView(focusedDate: $focusedDate)
YoteiScheduleView(focusedDate: $focusedDate, data: $data)
}
.environment(\.yoteiFontStyle, YoteiFontStyle(
caption: .system(.caption, design: .rounded),
caption2: .system(.caption2, design: .rounded),
body: .system(.body, design: .rounded),
headline: .system(.headline, design: .rounded).weight(.semibold),
subheadline: .system(.subheadline, design: .rounded)
))Scope styles to a single component:
YoteiStripContainerView(focusedDate: $focusedDate)
.environment(\.yoteiFontStyle, YoteiFontStyle(headline: .title3.bold()))Override individual styles:
YoteiScheduleView(focusedDate: $focusedDate, data: $data)
.environment(\.yoteiFontStyle.subheadline, .custom("Avenir-Heavy", size: 16))
.environment(\.yoteiFontStyle.caption2, .custom("Avenir-Book", size: 12))Every event-aware component accepts a view factory — a protocol with default implementations. Override only the methods you care about; the rest stay at their defaults.
import SwiftUI
import Yotei
struct BrandedDayEventsFactory: YoteiDayEventsViewFactoryProtocol {
private let palette: [Color] = [.red, .blue, .yellow, .green, .purple]
func eventView(event: YoteiEvent) -> some View {
let color = palette[abs(event.id.hashValue) % palette.count]
return YoteiDayEventsViewFactory()
.eventView(event: event)
.tint(color)
}
func timeSlotView(date: Date) -> some View {
MyCustomTimeSlotRow(date: date)
}
}Use it:
YoteiDayEventsView(
dayDate: focusedDate,
numberOfDays: 1,
data: $data,
contentOffset: $contentOffset,
viewFactory: BrandedDayEventsFactory()
)struct PurpleStripFactory: YoteiStripViewFactoryProtocol {
func expandView(isExpanded: Bool) -> some View {
YoteiStripViewFactory()
.expandView(isExpanded: isExpanded)
.foregroundStyle(.purple)
}
}
YoteiStripContainerView(
focusedDate: $focusedDate,
viewFactory: PurpleStripFactory()
)Because factories are plain structs with protocol-provided defaults, you can start by overriding a single method and add more as your design grows. You can also wrap the default factory (YoteiScheduleViewFactory(), YoteiDayEventsViewFactory(), etc.) and apply SwiftUI modifiers on top of its output instead of re-implementing a view from scratch.
Implement YoteiDelegate and pass it to calendar using .yoteiDelegate(_:) modifier:
final class CalendarCoordinator: YoteiDelegate {
func calendarDidSelectEvent(with id: YoteiEvent.ID) {
// Open event detail
}
func calendarDidSelectAllDay(date: Date) {
// Show the all-day list for that day
}
func calendarDidSelect(dateInterval: DateInterval, completion: () -> Void) {
// The user tapped an empty time slot — show a "new event" sheet.
// Call completion() to clear the placeholder when the sheet is dismissed.
}
func calendarDidSelectMonthDay(date: Date) {
// The user tapped a day cell in the month view — open the day's agenda or switch scope.
}
}A full example app is bundled in YoteiAppExample/. It demonstrates different usage examples and possible customization options.
To run it:
- Clone the repository:
git clone https://github.com/claustrofob/Yotei.git - Open
YoteiAppExample/YoteiAppExample.xcodeprojin Xcode 16 or newer. - Select an iOS 16+ simulator or device.
- Build and run (
⌘R).
The example project depends on the local Yotei package at the repo root, so any edits you make to Sources/ are picked up on the next build.
- Color customization for every component
- Custom views for events
- Stability improvements
- Font customization
- Month view
- Drag/drop to update event time/duration
- Accessibility
Copyright © 2026 Mikalai Zmachynski. All rights reserved.
Footnotes
-
Named after the Japanese word 予定 (yotei), meaning "schedule" or "planned event". ↩
