{"id":51099791,"url":"https://github.com/claustrofob/Yotei","last_synced_at":"2026-06-24T14:01:05.032Z","repository":{"id":349028271,"uuid":"1190713631","full_name":"claustrofob/Yotei","owner":"claustrofob","description":"A modular, customizable Swift calendar for iOS - SwiftUI API with UIKit performance. Date picker, schedule list, day timeline, week strip, and more.","archived":false,"fork":false,"pushed_at":"2026-05-08T12:33:46.000Z","size":756,"stargazers_count":126,"open_issues_count":0,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-05-29T00:28:38.261Z","etag":null,"topics":["calendar","calendar-component","calendar-view","datepicker","day-view","ios","ios16","schedule","schedule-view","spm","swift","swift-calendar","swiftui","swiftui-calendar","swiftui-components","timeline","uikit","week-view"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/claustrofob.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-24T14:49:15.000Z","updated_at":"2026-05-23T13:14:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/claustrofob/Yotei","commit_stats":null,"previous_names":["claustrofob/yotei"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/claustrofob/Yotei","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/claustrofob%2FYotei","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/claustrofob%2FYotei/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/claustrofob%2FYotei/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/claustrofob%2FYotei/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/claustrofob","download_url":"https://codeload.github.com/claustrofob/Yotei/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/claustrofob%2FYotei/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34735266,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-24T02:00:07.484Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["calendar","calendar-component","calendar-view","datepicker","day-view","ios","ios16","schedule","schedule-view","spm","swift","swift-calendar","swiftui","swiftui-calendar","swiftui-components","timeline","uikit","week-view"],"created_at":"2026-06-24T10:00:23.956Z","updated_at":"2026-06-24T14:01:05.026Z","avatar_url":"https://github.com/claustrofob.png","language":"Swift","funding_links":[],"categories":["Libs"],"sub_categories":["UI"],"readme":"# Yotei[^1]\n\n[![Swift](https://img.shields.io/badge/Swift-6.2+-orange.svg)](https://swift.org)\n[![Platform](https://img.shields.io/badge/Platform-iOS%2016+-lightgrey.svg)](https://developer.apple.com)\n[![SPM](https://img.shields.io/badge/SPM-compatible-brightgreen.svg)](https://swift.org/package-manager)\n[![License](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\nA highly modular, highly customizable calendar package for iOS. Built with SwiftUI and UIKit under the hood for the best performance and native feel.\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://github.com/user-attachments/assets/5577be55-33ef-4660-bff1-df030423a137\" width=\"256\" height=\"256\" alt=\"yotei-logo\"\u003e\n\u003c/p\u003e\n\nEvery 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.\n\n\u003cimg width=\"160\" alt=\"yotei_demo\" src=\"https://github.com/user-attachments/assets/1b624541-fbc4-4e53-bfc2-240a147dc7fe\" /\u003e\n\u003cimg width=\"160\" alt=\"day_view\" src=\"https://github.com/user-attachments/assets/e4c0838c-436d-48c4-93f0-803fc163282f\" /\u003e\n\u003cimg width=\"160\" alt=\"week_view\" src=\"https://github.com/user-attachments/assets/4397afb5-74e9-45f1-b694-494b84940def\" /\u003e\n\u003cimg width=\"160\" alt=\"month_view\" src=\"https://github.com/user-attachments/assets/ba5b8a4d-1221-4ce9-8ade-24a1e42d8a09\" /\u003e\n\u003cimg width=\"160\" alt=\"schedule_view\" src=\"https://github.com/user-attachments/assets/37eec48e-f59e-4620-ae99-fa4f5e4871d0\" /\u003e\n\n## Table of Contents\n\n- [Features](#features)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Typed Event Data](#typed-event-data)\n- [Colors Customization](#colors-customization)\n- [Fonts Customization](#fonts-customization)\n- [Customization with View Factories](#customization-with-view-factories)\n- [Handling User Interaction](#handling-user-interaction)\n- [Drag to Reschedule](#drag-to-reschedule)\n- [Example App](#example-app)\n- [Roadmap](#roadmap)\n- [License](#license)\n\n## Features\n\n- **Composable by design.** Each view is an independent SwiftUI `View` — use one, use several, arrange them however your layout requires.\n- **SwiftUI API, UIKit performance.** Heavy surfaces (scrolling schedule list, paging strip, date tabs) are backed by `UICollectionView` and `UIPageViewController` for smooth scrolling even with thousands of events.\n- **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.\n- **Calendar-aware.** Respects the `\\.calendar` environment, custom time zones, first-weekday settings, and locale-driven symbols.\n- **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.\n- **Modern Swift.** Swift 6.2, strict concurrency, `@MainActor`-correct factories, `Sendable` domain types.\n- **Production ready.** Support for iOS 16+ makes Yotei available not only for modern startups but even for mature projects.\n\n## Requirements\n\n- iOS 16+\n- Swift 6.2+\n- Xcode 16+\n\n## Installation\n\n### Swift Package Manager\n\nAdd Yotei to your `Package.swift`:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/claustrofob/Yotei.git\", branch: \"main\"),\n]\n```\n\nThen add it to your target:\n\n```swift\n.target(\n    name: \"YourApp\",\n    dependencies: [\"Yotei\"]\n)\n```\n\nOr in Xcode: **File → Add Package Dependencies…** and enter `https://github.com/claustrofob/Yotei.git`.\n\nThen import where needed:\n\n```swift\nimport Yotei\n```\n\n## Quick Start\n\nA minimal agenda-style calendar with the built-in strip, weekday titles, and a scrolling schedule list:\n\n```swift\nimport SwiftUI\nimport Yotei\n\nstruct CalendarScreen: View {\n    @State private var focusedDate = Date()\n    @State private var data = YoteiEventsInterval()\n\n    var body: some View {\n        VStack(spacing: 0) {\n            YoteiWeekdayTitlesView()\n            YoteiStripContainerView(focusedDate: $focusedDate)\n            YoteiScheduleView(\n                focusedDate: $focusedDate,\n                data: $data\n            )\n        }\n        .onChange(of: focusedDate) { _ in\n            // Load events for the visible month and assign them to `data.events`\n        }\n    }\n}\n```\n\nA standalone date picker with a min/max range:\n\n```swift\nimport SwiftUI\nimport Yotei\n\nstruct PickerScreen: View {\n    @State private var selectedDate = Date()\n\n    var body: some View {\n        YoteiDatePicker(\n            selectedDate: $selectedDate,\n            minDate: Calendar.current.date(byAdding: .day, value: -1, to: Date()),\n            maxDate: Calendar.current.date(byAdding: .month, value: 2, to: Date())\n        )\n        .padding()\n    }\n}\n```\n\nA full day view with an all-day header and a scrollable hour timeline:\n\n```swift\nYoteiPagesDayView(focusedDate: $focusedDate) { date in\n    VStack(spacing: 0) {\n        YoteiAllDayEventsTopView(\n            startDate: date,\n            numberOfDays: 1,\n            data: $data\n        )\n        YoteiDayEventsView(\n            dayDate: date,\n            numberOfDays: 1,\n            data: $data,\n            contentOffset: $contentOffset\n        )\n    }\n}\n```\n\nA full month grid with paging between months and multi-day event bars:\n\n```swift\nVStack(spacing: 0) {\n    YoteiWeekdayTitlesView()\n    YoteiPagesMonthView(focusedDate: $focusedDate) { date in\n        YoteiPagesMonthPageView(\n            selectedDate: $focusedDate,\n            data: $data,\n            dateInMonth: date\n        )\n    }\n}\n```\n\n## Typed Event Data\n\n`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:\n\n```swift\npublic struct YoteiEvent\u003cData: YoteiEventData\u003e: Equatable, Identifiable, Sendable {\n    public let id: String\n    public let title: String\n    public let start: Date\n    public let end: Date\n    public let isAllDay: Bool\n    public let data: Data\n}\n\npublic typealias YoteiEventData = Equatable \u0026 Sendable\n```\n\n`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`.\n\n### Attach your domain model\n\n```swift\nnonisolated struct EventPayload: Equatable, Sendable {\n    let calendarID: String\n    let tint: Color\n    let attendees: [String]\n    let isReadOnly: Bool\n}\n\nlet event = YoteiEvent(\n    id: \"evt-42\",\n    title: \"Design review\",\n    start: start,\n    end: end,\n    isAllDay: false,\n    data: EventPayload(\n        calendarID: \"work\",\n        tint: .indigo,\n        attendees: [\"alex\", \"sam\"],\n        isReadOnly: false\n    )\n)\n```\n\nThe generic parameter propagates through the whole pipeline so your payload is available at every extension point, fully typed.\n\n### Read your payload inside factories\n\n```swift\nstruct TintedDayEventsFactory: YoteiDayEventsViewFactoryProtocol {\n    func eventView(event: YoteiEvent\u003cEventPayload\u003e) -\u003e some View {\n        YoteiDayEventsViewFactory()\n            .eventView(event: event)\n            .tint(event.data.tint)\n            .opacity(event.data.isReadOnly ? 0.6 : 1.0)\n    }\n}\n```\n\n### Don't need extra data?\n\nUse an empty marker struct:\n\n```swift\nnonisolated struct EventData: Equatable, Sendable {}\n```\n\nYou pay nothing for the generic — the payload is a zero-sized field — and you can introduce real data later without rewriting any call sites.\n\n## Colors Customization\n\nEvery default view uses standard SwiftUI shape styles — `.tint`, `.background`, `.primary`, `.secondary`, `.tertiary` — so you can re-color the calendar with the standart SwiftUI modifiers:\n- `.foregroundStyle(_:_:_:)` - redefine .primary, .secondary and .tertiary styles\n- `.backgroundStyle(_:)` - redefine .background style\n- `.tint(_:)` - redefine .tint style\n\nYou have a few options to set custiom colors in calendar:\n- apply the above modifiers globally on calendar component\n- aply them on individual default views in [custom view factories](#customization-with-view-factories)\n- use your custom views with custom colors in view factories.\n\n### Examples\n\n```swift\nVStack(spacing: 0) {\n    YoteiWeekdayTitlesView()\n    YoteiStripContainerView(focusedDate: $focusedDate)\n    YoteiScheduleView(focusedDate: $focusedDate, data: $data)\n}\n.tint(.purple)\n```\n\nBecause `.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:\n\n```swift\nYoteiStripContainerView(focusedDate: $focusedDate)\n    .tint(.indigo)\n\nYoteiScheduleView(focusedDate: $focusedDate, data: $data)\n    .tint(.orange)\n```\n\nInside 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:\n\n```swift\nstruct BrandedDayEventsFactory: YoteiDayEventsViewFactoryProtocol {\n    func eventView(event: YoteiEvent\u003cEventPayload\u003e) -\u003e some View {\n        YoteiDayEventsViewFactory()\n            .eventView(event: event)\n            .tint(event.data.tint)\n    }\n}\n```\n\nDefault 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.\n\nFor anything finer-grained — borders, gradients, conditional colors per state — drop into a [view factory](#customization-with-view-factories) and override only the method you need.\n\n## Fonts Customization\n\nEvery default view renders text using a small, shared set of font roles exposed via `YoteiFontStyle`.\nThe 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.\n\nYou have a few options to set custom fonts in calendar:\n- inject a custom `YoteiFontStyle` globally on calendar component via the `\\.yoteiFontStyle` environment key\n- inject it on individual default views in [custom view factories](#customization-with-view-factories)\n- use your custom views with custom fonts in view factories.\n\n### Example\n\nApply a branded font style to the whole calendar:\n\n```swift\nVStack(spacing: 0) {\n    YoteiWeekdayTitlesView()\n    YoteiStripContainerView(focusedDate: $focusedDate)\n    YoteiScheduleView(focusedDate: $focusedDate, data: $data)\n}\n.environment(\\.yoteiFontStyle, YoteiFontStyle(\n    caption: .system(.caption, design: .rounded),\n    caption2: .system(.caption2, design: .rounded),\n    body: .system(.body, design: .rounded),\n    headline: .system(.headline, design: .rounded).weight(.semibold),\n    subheadline: .system(.subheadline, design: .rounded)\n))\n```\n\nScope styles to a single component:\n\n```swift\nYoteiStripContainerView(focusedDate: $focusedDate)\n    .environment(\\.yoteiFontStyle, YoteiFontStyle(headline: .title3.bold()))\n```\n\nOverride individual styles:\n\n```swift\nYoteiScheduleView(focusedDate: $focusedDate, data: $data)\n    .environment(\\.yoteiFontStyle.subheadline, .custom(\"Avenir-Heavy\", size: 16))\n    .environment(\\.yoteiFontStyle.caption2, .custom(\"Avenir-Book\", size: 12))\n```\n\n## Customization with View Factories\n\nEvery 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.\n\n### Example: custom-colored day-timeline events\n\n```swift\nimport SwiftUI\nimport Yotei\n\nstruct BrandedDayEventsFactory: YoteiDayEventsViewFactoryProtocol {\n    private let palette: [Color] = [.red, .blue, .yellow, .green, .purple]\n\n    func eventView(event: YoteiEvent) -\u003e some View {\n        let color = palette[abs(event.id.hashValue) % palette.count]\n        return YoteiDayEventsViewFactory()\n            .eventView(event: event)\n            .tint(color)\n    }\n\n    func timeSlotView(date: Date) -\u003e some View {\n        MyCustomTimeSlotRow(date: date)\n    }\n}\n```\n\nUse it:\n\n```swift\nYoteiDayEventsView(\n    dayDate: focusedDate,\n    numberOfDays: 1,\n    data: $data,\n    contentOffset: $contentOffset,\n    viewFactory: BrandedDayEventsFactory()\n)\n```\n\n### Example: custom strip expand indicator\n\n```swift\nstruct PurpleStripFactory: YoteiStripViewFactoryProtocol {\n    func expandView(isExpanded: Bool) -\u003e some View {\n        YoteiStripViewFactory()\n            .expandView(isExpanded: isExpanded)\n            .foregroundStyle(.purple)\n    }\n}\n\nYoteiStripContainerView(\n    focusedDate: $focusedDate,\n    viewFactory: PurpleStripFactory()\n)\n```\n\nBecause 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.\n\n## Handling User Interaction\n\nImplement `YoteiDelegate` and pass it to calendar using `.yoteiDelegate(_:)` modifier:\n\n```swift\nfinal class CalendarCoordinator: YoteiDelegate {\n    func calendarDidSelectEvent(with id: YoteiEvent.ID) {\n        // Open event detail\n    }\n\n    func calendarDidSelectAllDay(date: Date) {\n        // Show the all-day list for that day\n    }\n\n    func calendarDidSelect(dateInterval: DateInterval, completion: () -\u003e Void) {\n        // The user tapped an empty time slot — show a \"new event\" sheet.\n        // Call completion() to clear the placeholder when the sheet is dismissed.\n    }\n\n    func calendarDidSelectMonthDay(date: Date) {\n        // The user tapped a day cell in the month view — open the day's agenda or switch scope.\n    }\n\n    func calendarDidUpdateEvent(\n        with id: YoteiEvent.ID,\n        oldDateInterval: DateInterval,\n        newDateInterval: DateInterval\n    ) {\n        // The user dragged an event to a new time — persist the new interval.\n    }\n}\n```\n\n## Drag to Reschedule\n\n`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.\n\n### What it does for you\n\n- **Long-press + pan gesture.** A long press on an event activates drag mode, and the subsequent pan moves a floating proxy view.\n- **Auto-scroll near vertical edges.** When the finger gets close to the top or bottom of the visible area, the inner timeline scrolls automatically — proportional to how close you are to the edge — so you can drop an event onto a time that wasn't on screen.\n- **Page flip near horizontal edges.** When the finger lingers near the left or right edge, the focused date jumps one page (one day in `YoteiPagesDayView`, one week in `YoteiPagesWeekView`).\n- **Time snapping.** The new start time snaps to a configurable minute interval (default `15`).\n- **Cross-day moves.** Dropping an event onto a different day column moves it to that day; the duration is preserved.\n- **Delegate-based commit.** On release, Yotei calls `calendarDidUpdateEvent(with:oldDateInterval:newDateInterval:)` on your `YoteiDelegate`. Persist the change in your store and the calendar re-renders from the updated `data`.\n\n### Wiring it up\n\n`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.\n\nDay view with drag-to-reschedule:\n\n```swift\n@State private var focusedDate = Date()\n@State private var data = YoteiEventsInterval\u003cEventData\u003e()\n@State private var contentOffset: CGPoint?\n\nvar body: some View {\n    VStack(spacing: 0) {\n        YoteiWeekdayTitlesView()\n        YoteiStripContainerView(focusedDate: $focusedDate)\n        YoteiDragEventView(\n            data: $data,\n            contentOffset: $contentOffset,\n            focusedDate: $focusedDate\n        ) {\n            YoteiPagesDayView(focusedDate: $focusedDate) { date in\n                VStack(spacing: 0) {\n                    YoteiAllDayEventsTopView(\n                        startDate: date,\n                        numberOfDays: 1,\n                        data: $data\n                    )\n                    YoteiDayEventsView(\n                        dayDate: date,\n                        numberOfDays: 1,\n                        data: $data,\n                        contentOffset: $contentOffset\n                    )\n                }\n            }\n        }\n    }\n    .yoteiDelegate(coordinator)\n}\n```\n\n### Persisting the new time\n\nImplement `calendarDidUpdateEvent` on your `YoteiDelegate` and update the event in your store. Once `data.events` reflects the move, the calendar re-renders automatically:\n\n```swift\nfinal class CalendarCoordinator: YoteiDelegate {\n    func calendarDidUpdateEvent(\n        with id: YoteiEvent\u003cEventData\u003e.ID,\n        oldDateInterval: DateInterval,\n        newDateInterval: DateInterval\n    ) {\n        store.updateEvent(id: id, newStart: newDateInterval.start, newEnd: newDateInterval.end)\n    }\n\n    // … other YoteiDelegate methods …\n}\n```\n\n## Example App\n\nA full example app is bundled in `YoteiAppExample/`. It demonstrates different usage examples and possible customization options.\n\nTo run it:\n\n1. Clone the repository: `git clone https://github.com/claustrofob/Yotei.git`\n2. Open `YoteiAppExample/YoteiAppExample.xcodeproj` in Xcode 16 or newer.\n3. Select an iOS 16+ simulator or device.\n4. Build and run (`⌘R`).\n\nThe 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.\n\n## Roadmap\n\n- [x] Color customization for every component\n- [x] Custom views for events\n- [x] Stability improvements\n- [x] Font customization\n- [x] Month view\n- [x] Drag/drop to update event time/duration\n- [ ] Accessibility\n\n## License\n\nCopyright © 2026 Mikalai Zmachynski. All rights reserved.\n\n[^1]: Named after the Japanese word 予定 (yotei), meaning \"schedule\" or \"planned event\".\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclaustrofob%2FYotei","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fclaustrofob%2FYotei","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fclaustrofob%2FYotei/lists"}