ForwardWidgets
Widgets for Douban and Trakt watchlists plus personalized recommendations, live TV streaming including PlutoTV, Yatu ra…
A modular, customizable Swift calendar for iOS - SwiftUI API with UIKit performance. Date picker, schedule list, day ti…
git clone https://github.com/claustrofob/Yotei.gitclaustrofob/YoteiA 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.
View — use one, use several, arrange them however your layout requires.UICollectionView and UIPageViewController for smooth scrolling even with thousands of events.\.calendar environment, custom time zones, first-weekday settings, and locale-driven symbols.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.@MainActor-correct factories, Sendable domain types.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 Yotei
A 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(
startDate: 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
)
}
}
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 & Sendable
Data 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)
}
}
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 styleYou have a few options to set custiom colors in calendar:
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:
YoteiFontStyle globally on calendar component via the \.yoteiFontStyle environment keyApply 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(
startDate: 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.
}
func calendarDidUpdateEvent(
with id: YoteiEvent.ID,
oldDateInterval: DateInterval,
newDateInterval: DateInterval
) {
// The user dragged an event to a new time — persist the new interval.
}
}
YoteiDragEventView adds drag-to-reschedule to a day or week timeline without forcing you to rebuild the timeline itself. Wrap it around an existing YoteiPagesDayView / YoteiPagesWeekView (with YoteiDayEventsView inside) and the new gesture layer is live — long-press an event to "pick it up", drag it to a new time slot, release to commit.
YoteiPagesDayView, one week in YoteiPagesWeekView).15).calendarDidUpdateEvent(with:oldDateInterval:newDateInterval:) on your YoteiDelegate. Persist the change in your store and the calendar re-renders from the updated data.YoteiDragEventView needs the same three bindings the underlying timeline uses (data, contentOffset, focusedDate) so it can read event frames, drive auto-scroll, and trigger page flips. Keep the contained YoteiPagesDayView / YoteiPagesWeekView and YoteiDayEventsView exactly as you had them — just wrap them.
Day view with drag-to-reschedule:
@State private var focusedDate = Date()
@State private var data = YoteiEventsInterval<EventData>()
@State private var contentOffset: CGPoint?
var body: some View {
VStack(spacing: 0) {
YoteiWeekdayTitlesView()
YoteiStripContainerView(focusedDate: $focusedDate)
YoteiDragEventView(
data: data,
contentOffset: $contentOffset,
focusedDate: $focusedDate
) {
YoteiPagesDayView(focusedDate: $focusedDate) { date in
VStack(spacing: 0) {
YoteiAllDayEventsTopView(
startDate: date,
numberOfDays: 1,
data: data
)
YoteiDayEventsView(
startDate: date,
numberOfDays: 1,
data: data,
contentOffset: $contentOffset
)
}
}
}
}
.yoteiDelegate(coordinator)
}
Implement calendarDidUpdateEvent on your YoteiDelegate and update the event in your store. Once data.events reflects the move, the calendar re-renders automatically:
final class CalendarCoordinator: YoteiDelegate {
func calendarDidUpdateEvent(
with id: YoteiEvent<EventData>.ID,
oldDateInterval: DateInterval,
newDateInterval: DateInterval
) {
store.updateEvent(id: id, newStart: newDateInterval.start, newEnd: newDateInterval.end)
}
// … other YoteiDelegate methods …
}
A full example app is bundled in YoteiAppExample/. It demonstrates different usage examples and possible customization options.
To run it:
git clone https://github.com/claustrofob/Yotei.gitYoteiAppExample/YoteiAppExample.xcodeproj in Xcode 16 or newer.⌘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.
Copyright © 2026 Mikalai Zmachynski. All rights reserved.
Named after the Japanese word 予定 (yotei), meaning "schedule" or "planned event". ↩
more like this
Widgets for Douban and Trakt watchlists plus personalized recommendations, live TV streaming including PlutoTV, Yatu ra…
Vibe-coded, but not slop. A mobile-first SDR receiver for iOS, Android & Apple Watch — the live waterfall on your wrist…
A lightweight, simplified, RedNotebook-inspired journal/diary app. Built with Electron. Makes dated text files.
search projects, people, and tags