• Stars
    star
    164
  • Rank 223,493 (Top 5 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 2 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Simple calendar for SwiftUI

SwiftUICalendar

Version License Platform

SwiftUICalendar is calendar view for SwiftUI

Installation

Swift Package Manager

  • From url : https://github.com/GGJJack/SwiftUICalendar

or

  • Package.swift
.package(name: "SwiftUICalendar", url: "https://github.com/GGJJack/SwiftUICalendar", from: "0.1.14")

CocoaPods

pod 'SwiftUICalendar'

import

import SwiftUICalendar

Features

  • Infinite scroll
  • Support horizontal and vertical scroll
  • Full custom calendar cell
  • Pager lock
  • Starting the week with Sunday or Monday

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")
                // .onChange(of: controller.yearMonth) { yearMonth in // If you want to detect date change
                //     print(yearMonth)
                // }
            }
        }
    }
}

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

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

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
        }
    }
}

Date change detection

CalendarView(controller) { date in
 ....
}
.onChange(of: controller.yearMonth) { yearMonth in
    print(yearMonth)
}

Start with Monday

Basic Use

Show example code

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

    var body: some View {
        GeometryReader { reader in
            VStack {
                Text("\(controller.yearMonth.monthShortString), \(String(controller.yearMonth.year))")
                    .font(.title)
                    .padding(EdgeInsets(top: 8, leading: 0, bottom: 8, trailing: 0))

                CalendarView(controller, startWithMonday: true, headerSize: .fixHeight(50.0)) { week in
                    Text("\(week.shortString)")
                        .font(.headline)
                        .frame(width: reader.size.width / 7)
                } component: { date in
                    GeometryReader { geometry in
                        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(.black)
                                .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                        }
                    }
                }
            }
        }
    }
}

Show example code

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

    var body: some View {
        GeometryReader { reader in
            VStack {
                HStack(alignment: .center, spacing: 0) {
                    ForEach(0..<7, id: \.self) { i in
                        Text(DateFormatter().shortWeekdaySymbols[i < 6 ? i + 1 : 0])
                            .font(.headline)
                            .frame(width: reader.size.width / 7)
                    }
                }
                CalendarView(controller, startWithMonday: true, headerSize: .fixHeight(50.0)) { week in
                    Text("\(week.shortString)")
                        .font(.headline)
                        .frame(width: reader.size.width / 7)
                } component: { date in
                    GeometryReader { geometry in
                        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(.black)
                                .opacity(date.isFocusYearMonth == true ? 1 : 0.4)
                        }
                    }
                }
            }
        }
    }
}

Struct

CalendarView

public struct CalendarView<CalendarCell: View, HeaderCell: View>: View {
    public init(
        _ controller: CalendarController = CalendarController(),
        startWithMonday: Bool = false,
        @ViewBuilder component: @escaping (YearMonthDay) -> CalendarCell
    ) {
        ...
    }
    public init(
        _ controller: CalendarController = CalendarController(),
        startWithMonday: Bool = false,
        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
}

Contributors

Thank you very much for your contribution! πŸ™

@aaron25mt @NSRover @sanhee16

Author

GGJJack, [email protected]

License

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