SwiftUICalendar - SwiftUI simple calendar

Overview

SwiftUICalendar

Version License Platform

Installation

CocoaPods

pod 'SwiftUICalendar'

import

import SwiftUICalendar

Features

  • Infinite scroll
  • Support horizontal and vertical scroll
  • Full custom calendar cell
  • Pager lock

Example

Basic

CalendarView() { date in
    Text("\(date.day)")
}

Basic use

Basic Use

Show example code

struct BasicUseView: View {
    @ObservedObject var controller: CalendarController = CalendarController(orientation: .vertical)
    
    var body: some View {
        GeometryReader { reader in
            VStack(alignment: .center, spacing: 0) {
                Text("\(controller.yearMonth.monthShortString), \(String(controller.yearMonth.year))")
                    .font(.title)
                    .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
                HStack(alignment: .center, spacing: 0) {
                    ForEach(0..<7, id: \.self) { i in
                        Text(DateFormatter().shortWeekdaySymbols[i])
                            .font(.headline)
                            .frame(width: reader.size.width / 7)
                    }
                }
                CalendarView(controller) { date in
                    GeometryReader { geometry in
                        ZStack(alignment: .center) {
                            if date.isToday {
                                Circle()
                                    .padding(4)
                                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                                    .foregroundColor(.orange)
                                Text("\(date.day)")
                                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                                    .font(.system(size: 10, weight: .bold, design: .default))
                                    .foregroundColor(.white)
                            } else {
                                Text("\(date.day)")
                                    .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                                    .font(.system(size: 10, weight: .light, design: .default))
                                    .foregroundColor(getColor(date))
                                    .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                            }
                        }
                    }
                }
            }
        }
    }
}

Calendar scroll

Basic Use

Show example code

struct CalendarScrollView: View {
    @ObservedObject var controller: CalendarController = CalendarController()
    
    var body: some View {
        GeometryReader { reader in
            VStack(alignment: .center, spacing: 0) {
                HStack(alignment: .center, spacing: 0) {
                    Spacer()
                    Button("Older") {
                        controller.scrollTo(YearMonth(year: 1500, month: 1), isAnimate: true)
                    }
                    Spacer()
                    Button("Today") {
                        controller.scrollTo(YearMonth.current, isAnimate: false)
                    }
                    Spacer()
                    Button("Today Scroll") {
                        controller.scrollTo(YearMonth.current, isAnimate: true)
                    }
                    Spacer()
                    Button("Future") {
                        controller.scrollTo(YearMonth(year: 2500, month: 1), isAnimate: true)
                    }
                    Spacer()
                }
                Text("\(controller.yearMonth.monthShortString), \(String(controller.yearMonth.year))")
                    .font(.title)
                    .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
                HStack(alignment: .center, spacing: 0) {
                    ForEach(0..<7, id: \.self) { i in
                        Text(DateFormatter().shortWeekdaySymbols[i])
                            .font(.headline)
                            .frame(width: reader.size.width / 7)
                    }
                }
                CalendarView(controller) { date in
                    GeometryReader { geometry in
                        Text("\(date.day)")
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                            .font(.system(size: 10, weight: .light, design: .default))
                            .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                    }
                }
                .navigationBarTitle("Calendar Scroll")
            }
        }
    }
}

Embed Header

Basic Use

Show example code

struct EmbedHeaderView: View {
    
    @ObservedObject var controller: CalendarController = CalendarController()

    var body: some View {
        GeometryReader { reader in
            VStack {
                HStack(alignment: .center, spacing: 0) {
                    Button("Prev") {
                        controller.scrollTo(controller.yearMonth.addMonth(value: -1), isAnimate: true)
                    }
                    .padding(8)
                    Spacer()
                    Text("\(controller.yearMonth.monthShortString), \(String(controller.yearMonth.year))")
                        .font(.title)
                        .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))
                    Spacer()
                    Button("Next") {
                        controller.scrollTo(controller.yearMonth.addMonth(value: 1), isAnimate: true)
                    }
                    .padding(8)
                }
                CalendarView(controller, header: { week in
                    GeometryReader { geometry in
                        Text(week.shortString)
                            .font(.subheadline)
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                    }
                }, component: { date in
                    GeometryReader { geometry in
                        Text("\(date.day)")
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                            .font(.system(size: 10, weight: .light, design: .default))
                            .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                    }
                })
            }
        }
        .navigationBarTitle("Embed header")
    }
}

Information

Basic Use

Show example code

Color { if date.dayOfWeek == .sun { return Color.red } else if date.dayOfWeek == .sat { return Color.blue } else { return Color.black } } } ">
extension YearMonthDay: Hashable {
    public func hash(into hasher: inout Hasher) {
        hasher.combine(self.year)
        hasher.combine(self.month)
        hasher.combine(self.day)
    }
}

struct InformationView: View {
    var informations = [YearMonthDay: [(String, Color)]]()
    
    init() {
        var date = YearMonthDay.current
        informations[date] = []
        informations[date]?.append(("Hello", Color.orange))
        informations[date]?.append(("World", Color.blue))

        date = date.addDay(value: 3)
        informations[date] = []
        informations[date]?.append(("Test", Color.pink))
        
        date = date.addDay(value: 8)
        informations[date] = []
        informations[date]?.append(("Jack", Color.green))
        
        date = date.addDay(value: 5)
        informations[date] = []
        informations[date]?.append(("Home", Color.red))

        date = date.addDay(value: -23)
        informations[date] = []
        informations[date]?.append(("Meet at 8, Home", Color.purple))
        
        date = date.addDay(value: -5)
        informations[date] = []
        informations[date]?.append(("Home", Color.yellow))

        date = date.addDay(value: -10)
        informations[date] = []
        informations[date]?.append(("Baseball", Color.green))
    }

    var body: some View {
        GeometryReader { reader in
            VStack {
                CalendarView(header: { week in
                    GeometryReader { geometry in
                        Text(week.shortString)
                            .font(.subheadline)
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                    }
                }, component: { date in
                    GeometryReader { geometry in
                        VStack(alignment: .leading, spacing: 2) {
                            if date.isToday {
                                Text("\(date.day)")
                                    .font(.system(size: 10, weight: .bold, design: .default))
                                    .padding(4)
                                    .foregroundColor(.white)
                                    .background(Color.red.opacity(0.95))
                                    .cornerRadius(14)
                            } else {
                                Text("\(date.day)")
                                    .font(.system(size: 10, weight: .light, design: .default))
                                    .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                                    .foregroundColor(getColor(date))
                                    .padding(4)
                            }
                            if let infos = informations[date] {
                                ForEach(infos.indices) { index in
                                    let info = infos[index]
                                    Text(info.0)
                                        .lineLimit(1)
                                        .foregroundColor(.white)
                                        .font(.system(size: 8, weight: .bold, design: .default))
                                        .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
                                        .frame(width: geometry.size.width, alignment: .center)
                                        .background(info.1.opacity(0.75))
                                        .cornerRadius(4)
                                        .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                                }
                            }
                        }
                        .frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
                    }
                })
            }
        }
        .navigationBarTitle("Information")
    }
    
    private func getColor(_ date: YearMonthDay) -> Color {
        if date.dayOfWeek == .sun {
            return Color.red
        } else if date.dayOfWeek == .sat {
            return Color.blue
        } else {
            return Color.black
        }
    }
}

Selection

Basic Use

Show example code

struct SelectionView: View {
    @ObservedObject var controller: CalendarController = CalendarController()
    @State var focusDate: YearMonthDay? = YearMonthDay.current
    
    var body: some View {
        GeometryReader { reader in
            VStack {
                CalendarView(controller, header: { week in
                    GeometryReader { geometry in
                        Text(week.shortString)
                            .font(.subheadline)
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                    }
                }, component: { date in
                    GeometryReader { geometry in
                        Text("\(date.day)")
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                            .font(.system(size: 10, weight: .light, design: .default))
                            .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                            .border(.green.opacity(0.8), width: (focusDate == date ? 1 : 0))
                            .cornerRadius(2)
                            .contentShape(Rectangle())
                            .onTapGesture {
                                focusDate = (date != focusDate ? date : nil)
                            }
                    }
                })
            }
        }
        .navigationBarTitle("Selection")
    }
}

Information + Selection

Basic Use

Show example code

Color { if date.dayOfWeek == .sun { return Color.red } else if date.dayOfWeek == .sat { return Color.blue } else { return Color.black } } } ">
struct InformationWithSelectionView: View {
    let controller = CalendarController()
    var informations = [YearMonthDay: [(String, Color)]]()
    @State var focusDate: YearMonthDay? = nil
    @State var focusInfo: [(String, Color)]? = nil

    init() {
        var date = YearMonthDay.current
        informations[date] = []
        informations[date]?.append(("Hello", Color.orange))
        informations[date]?.append(("World", Color.blue))

        date = date.addDay(value: 3)
        informations[date] = []
        informations[date]?.append(("Test", Color.pink))
        
        date = date.addDay(value: 8)
        informations[date] = []
        informations[date]?.append(("Jack", Color.green))
        
        date = date.addDay(value: 5)
        informations[date] = []
        informations[date]?.append(("Home", Color.red))

        date = date.addDay(value: -23)
        informations[date] = []
        informations[date]?.append(("Meet at 8, Home", Color.purple))
        
        date = date.addDay(value: -5)
        informations[date] = []
        informations[date]?.append(("Home", Color.yellow))

        date = date.addDay(value: -10)
        informations[date] = []
        informations[date]?.append(("Baseball", Color.green))
    }

    var body: some View {
        GeometryReader { reader in
            VStack {
                CalendarView(controller, header: { week in
                    GeometryReader { geometry in
                        Text(week.shortString)
                            .font(.subheadline)
                            .frame(width: geometry.size.width, height: geometry.size.height, alignment: .center)
                    }
                }, component: { date in
                    GeometryReader { geometry in
                        VStack(alignment: .leading, spacing: 2) {
                            if date.isToday {
                                Text("\(date.day)")
                                    .font(.system(size: 10, weight: .bold, design: .default))
                                    .padding(4)
                                    .foregroundColor(.white)
                                    .background(Color.red.opacity(0.95))
                                    .cornerRadius(14)
                            } else {
                                Text("\(date.day)")
                                    .font(.system(size: 10, weight: .light, design: .default))
                                    .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                                    .foregroundColor(getColor(date))
                                    .padding(4)
                            }
                            if let infos = informations[date] {
                                ForEach(infos.indices) { index in
                                    let info = infos[index]
                                    if focusInfo != nil {
                                        Rectangle()
                                            .fill(info.1.opacity(0.75))
                                            .frame(width: geometry.size.width, height: 4, alignment: .center)
                                            .cornerRadius(2)
                                            .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                                    } else {
                                        Text(info.0)
                                            .lineLimit(1)
                                            .foregroundColor(.white)
                                            .font(.system(size: 8, weight: .bold, design: .default))
                                            .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
                                            .frame(width: geometry.size.width, alignment: .center)
                                            .background(info.1.opacity(0.75))
                                            .cornerRadius(4)
                                            .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                                    }
                                }
                            }
                        }
                        .frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
                        .border(.green.opacity(0.8), width: (focusDate == date ? 1 : 0))
                        .cornerRadius(2)
                        .contentShape(Rectangle())
                        .onTapGesture {
                            withAnimation {
                                if focusDate == date {
                                    focusDate = nil
                                    focusInfo = nil
                                } else {
                                    focusDate = date
                                    focusInfo = informations[date]
                                }
                            }
                        }
                    }
                })
                if let infos = focusInfo {
                    List(infos.indices, id: \.self) { index in
                        let info = infos[index]
                        HStack(alignment: .center, spacing: 0) {
                            Circle()
                                .fill(info.1.opacity(0.75))
                                .frame(width: 12, height: 12)
                            Text(info.0)
                                .padding(.leading, 8)
                        }
                    }
                    .frame(width: reader.size.width, height: 160, alignment: .center)
                }
            }
        }
        .navigationBarTitle("Info + Select")
    }
    
    private func getColor(_ date: YearMonthDay) -> Color {
        if date.dayOfWeek == .sun {
            return Color.red
        } else if date.dayOfWeek == .sat {
            return Color.blue
        } else {
            return Color.black
        }
    }
}

Struct

CalendarView

public struct CalendarView<CalendarCell: View, HeaderCell: View>: View {
    public init(
        _ controller: CalendarController = CalendarController(),
        @ViewBuilder component: @escaping (YearMonthDay) -> CalendarCell
    ) {
        ...
    }
    public init(
        _ controller: CalendarController = CalendarController(),
        headerSize: HeaderSize = .fixHeight(40),
        @ViewBuilder header: @escaping (Week) -> HeaderCell,
        @ViewBuilder component: @escaping (YearMonthDay) -> CalendarCell
    ) {
        ...
    }
    
    ...
}

HeaderSize

public enum HeaderSize {
    case zero
    case ratio
    case fixHeight(CGFloat)
}

CalendarController

public class CalendarController: ObservableObject {
    public init(
        _ yearMonth: YearMonth = .current, 
        orientation: Orientation = .horizontal,
        isLocked: Bool = false
    )
    
    ...
}

var verticalController = CalendarController(
    YearMonth.current,
    orientation: .vertical,
    isLocked: true
)

var controller = CalendarController()

// Scroll with animate
controller.scrollTo(year: 1991, month: 2, isAnimate: true)

// Scroll without animate
controller.scrollTo(YearMonth.current, isAnimate: false)

// Lock Pager
controller.isLocked = true

YearMonth

public struct YearMonth: Equatable {
    public let year: Int
    public let month: Int
    
    public init(year: Int, month: Int) { ... }
    
    ...
}

let date = YearMonth(year: 2021, month: 10)
let now = YearMonth.current // Now

print(date.monthShortString) // Oct // Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec

let nextMonth = date.addMonth(value: 1) // {year: 2021, month: 11}

let diff = nextMonth.diffMonth(value: date) // 2021/11 - 2021/10 = 1

let components: DateComponents = nextMonth.toDateComponents()

YearMonthDay

public struct YearMonthDay: Equatable {
    public let year: Int
    public let month: Int
    public let day: Int
    public let isFocusYearMonth: Bool?
    
    public init(year: Int, month: Int, day: Int) { ... }
        
    public init(year: Int, month: Int, day: Int, isFocusYearMonth: Bool) { ... }
    
    ...
}

let date = YearMonthDay(year: 2021, month: 10, day: 26)
let now = YearMonthDay.current

let isToday = now.isToday // true

let dayOfWeek: Week = date.dayOfWeek // Tue // Sun, Mon, Tue, Wed, Thu, Fri, Sat

let toDate = date.date! // { 2021/10/26 }

let components: DateComponents = date.toDateComponents()

let tomorrow = date.addDay(value: 1) // {year: 2021, month: 10, day: 27}

let diff = tomorrow.diffDay(value: date) // 2021/10/27 - 2021/10/26 = 1

Week

public enum Week: Int, CaseIterable {
    case sun = 0
    case mon = 1
    case tue = 2
    case wed = 3
    case thu = 4
    case fri = 5
    case sat = 6
    
    public var shortString: String // Sun, Mon, Tue, Wed, Thu, Fri, Sat
}

Author

GGJJack, [email protected]

License

SwiftUICalendar is available under the MIT license. See the LICENSE file for more info.

You might also like...
Clendar - universal calendar app. Written in SwiftUI. Available on App Store. MIT License.

Clendar - minimal calendar Minimal Calendar & Widgets Landing Page About This project is started out as an UIKit base app for me to learn new WWDC fea

A SwiftUI calendar view that allows month switching and date picking.
A SwiftUI calendar view that allows month switching and date picking.

Selectable Calendar View A SwiftUI calendar view that allows month switching and date picking. Usage You can simply add this repository to your projec

📅 Calendar for iOS, iPadOS and macOS in Swift
📅 Calendar for iOS, iPadOS and macOS in Swift

CalendarKit CalendarKit is a Swift calendar UI library for iOS, iPadOS and Mac Catalyst. It looks similar to the Apple Calendar app out-of-the-box, wh

An Easy to Use Calendar for iOS (Swift 5.0)
An Easy to Use Calendar for iOS (Swift 5.0)

This is an easy to use, "just drag and drop it in your code" type of calendar for iOS. It supports both vertical and horizontal scrolling, as well as

A calendar quick view for the MacOS status bar

Calendar Quick View Quick Menu Calendar in the mac app store An open source macOS calendar preview utility Download from the Mac App Store Visualizati

A custom visual calendar for iOS 8+ written in Swift (>= 4.0).
A custom visual calendar for iOS 8+ written in Swift (= 4.0).

Overview Screenshots GIF Demo Installation Usage Architecture Version matrix Advanced API For contributors Screenshots GIF Demo Installation CocoaPods

iOS 7+ Calendar (Date Picker) with Infinite Scrolling.
iOS 7+ Calendar (Date Picker) with Infinite Scrolling.

RSDayFlow iOS 7 Calendar with Infinite Scrolling. Only need 4 lines of code to set up. RSDayFlow is a slim fork of DayFlow with updates and extensions

An availability calendar implementation for iOS
An availability calendar implementation for iOS

NWCalendarView NWCalendar View is an iOS control that displays a calendar. It is perfect for appointment or availibilty selection. It allows for selec

A fully customizable calendar view acting as a date range picker
A fully customizable calendar view acting as a date range picker

Demo Installation CocoaPods With CocoaPods you can simply add GLCalendarView in your Podfile: pod "GLCalendarView", "~ 1.0.0" Source File You can co

Comments
  • Is it possible to detect the calendar has been changed by using gesture

    Is it possible to detect the calendar has been changed by using gesture

    The Calendar Scroll example is buttons, so you can use buttons to switch the calendar view and then trigger actions to update information on the calendar.

    https://github.com/GGJJack/SwiftUICalendar#calendar-scroll

    But in this case, if the user tries to swipe the calendar, there is no way to detect this gesture and do related actions. Is there any way to achieve this? Thank you!

    opened by EragonJ 2
  • YearMonthDay needs to conform to Hashable

    YearMonthDay needs to conform to Hashable

    In order to use YearMonthDay as key for a dictionary, as shown in the example:

    var informations = [YearMonthDay: [(String, Color)]]()
    

    it needs to conform to Hashable protocol.

    Compiler Error: "Generic struct 'Dictionary' requires that 'YearMonthDay' conform to 'Hashable'"

    opened by NSRover 0
  • Continuous scroll instead of pages

    Continuous scroll instead of pages

    Great library, simple usage!

    I have this vertical calendar:

    https://user-images.githubusercontent.com/2267270/206548240-fbfc22d4-5485-44fe-b7ff-50f76122dbd2.mp4

    As you can see my last visible date is 18 under the floating button. How can I achieve when I scroll the calendar to continue from 19 instead of 1 (can you see on the video)

    opened by azlekov 0
  • Rotation change current month

    Rotation change current month

    Hello,

    I was trying to use your sample of Information+Selection in an iPad app, and I realized that the controller yearMonth is changing on device rotation.

    Same thing happens on the sample project without any modification.

    It is noticeable looking at the first day of the month.

    opened by a-Mo7 0
Owner
null
RCalendarPicker A date picker control, Calendar calendar control, select control, calendar, date selection, the clock selection control.

RCalendarPicker RCalendarPicker Calendar calendar control, select control, calendar, date selection, the clock selection control. 日历控件 ,日历选择控件,日历,日期选择

杜耀辉 131 Jul 18, 2022
A declarative, performant, iOS calendar UI component that supports use cases ranging from simple date pickers all the way up to fully-featured calendar apps.

HorizonCalendar A declarative, performant, calendar UI component that supports use cases ranging from simple date pickers all the way up to fully-feat

Airbnb 2.2k Jan 4, 2023
Simple customizable calendar component in Swift :calendar:

Koyomi Koyomi is a simple calendar view framework for iOS, written in Swift ?? Content Features Demo App Usage introduction : Change displayed month,

Shohei Yokoyama 741 Dec 24, 2022
Malendar is a personal calendar app that connects to your default calendar and lets you add/delete events

Malendar is a personal calendar app that connects to your default calendar and lets you add/delete events. It will gather events from your default iOS calendar.

Chase 194 Jan 4, 2023
SwiftUI Simple Calendar / Date Picker for iOS

RKCalendar RKCalendar is a SwiftUI Calendar / Date Picker for iOS. Features include: minimum and maximum calendar dates selectable, single date select

null 453 Dec 28, 2022
Dead simple calendar implementation

Package provides a CalendarView which can be used to display simple calendar in your App.

Sergei Kononov 4 Oct 7, 2022
Calendar View - It's lightweight and simple control with supporting Locale and CalendarIdentifier.

iOS Calendar It's lightweight and simple control with supporting Locale and CalendarIdentifier. There're samples for iPhone and iPad, and also with us

Maksym Bilan 159 Dec 22, 2022
The elegant full screen calendar missed in SwiftUI.

ElegantCalendar ElegantCalendar is an efficient and customizable full screen calendar written in SwiftUI. ElegantTimeline - Shows what's possible usin

Kevin Li 553 Dec 27, 2022
A customizable swiftui calendar

Description not available. Installation From Xcode 11, you can use Swift Package Manager to add Kingfisher to your project. Select File > Swift Packag

Heshan Yodagama 140 Dec 24, 2022
An easy to use SwiftUI date picker for Shamsi (Persian) calendar

ShamsiDatePicker An easy-to-use SwiftUI iOS/watchOS date picker for Shamsi (Persian) calendar. Features Pure (100%) SwiftUI implementation Full suppor

Seyyed Parsa Neshaei 20 Nov 24, 2022