Skip to content

Instantly share code, notes, and snippets.

@Jxrgxn
Created December 6, 2020 21:57
Show Gist options
  • Save Jxrgxn/ff9f02137604319b2f5f76831fa3b630 to your computer and use it in GitHub Desktop.
Save Jxrgxn/ff9f02137604319b2f5f76831fa3b630 to your computer and use it in GitHub Desktop.
//
// Calendar+Extension.swift
// Align-iOS
//
// Created by Basel Farag on 12/6/20.
//
import Foundation
extension Calendar {
func firstDayInAWeekContaining(date: Date) -> Date {
let weekdayOfTheDate = component(.weekday, from: date)
let numberOfDaysAheadOfTheBeginningOfTheWeek = weekdayOfTheDate - firstWeekday
return date.adding(days: -numberOfDaysAheadOfTheBeginningOfTheWeek)
}
func firstDay(ofMonth month: Int, year: Int) -> Date {
var components = DateComponents()
components.month = month
components.year = year
components.day = 1
components.hour = 12
return date(from: components)!
}
func lastDay(ofMonth month: Int, year: Int) -> Date {
var components = DateComponents()
components.month = month
components.year = year
components.day = 1
components.hour = 12
let newComponents = components.addingMonth()!
return date(from: newComponents)!.adding(days: -1)
}
func numberOfWeeks(inMonth month: Int, year: Int) -> Int {
let date = lastDay(ofMonth: month, year: year)
let weekRange = range(of: .weekOfMonth,
in: .month,
for: date)
return weekRange!.count
}
func isDate(_ dateA: Date, theSameAs dateB: Date, inComponents components: Set<Calendar.Component>) -> Bool {
return dateComponents(components, from: dateA) == dateComponents(components, from: dateB)
}
}
extension TimeInterval {
static let hoursInADay: TimeInterval = 24
static let secondsInAnHour: TimeInterval = 3600
static let secondsInADay: TimeInterval = 24 * 3600
}
extension Date {
func adding(days: Int) -> Date {
return addingTimeInterval(.secondsInADay * TimeInterval(days))
}
}
extension DateComponents {
func addingMonth() -> DateComponents? {
guard let month = month, let year = year else {
return nil
}
var newComponents = self
if month < 12 {
newComponents.month = month + 1
} else {
newComponents.year = year + 1
newComponents.month = 1
}
return newComponents
}
}
extension Date {
static func date(day: Int, month: Int, year: Int, hour: Int = 14) -> Date {
var components = DateComponents()
components.day = day
components.month = month
components.year = year
components.hour = hour
return Calendar.current.date(from: components)!
}
}
// CalendarView.swift
// Align-iOS
//
// Created by Basel Farag on 12/5/20.
//
import SwiftUI
extension Date {
var dayKey: UInt {
let components = Calendar.current.dateComponents([.day, .month, .year], from: self)
return UInt(components.day! + 1000000 + components.month! * 10000 + components.year!)
}
}
struct CalendarView: View {
private let calendar: Calendar = .current
func goToToday() {
currentMonth = calendar.component(.month, from: Date())
currentYear = calendar.component(.year, from: Date())
}
@State var currentMonth: Int = Calendar.current.component(.month, from: Date())
@State var currentYear: Int = Calendar.current.component(.year, from: Date())
@Binding var selectedDay: Date?
let appointmentsByDay: [UInt: [Appointment]]
var weeksCount: Int {
return calendar.numberOfWeeks(inMonth: currentMonth,
year: currentYear)
}
var body: some View {
GeometryReader { geometry in
let itemSize = geometry.size.width / CGFloat(calendar.veryShortWeekdaySymbols.count)
VStack(spacing: 0) {
HStack {
// Month picker
// ------------
Button(action: {
if currentMonth > 1 {
currentMonth -= 1
} else {
currentYear -= 1
currentMonth = 12
}
}, label: {
Image(systemName: "chevron.left")
})
Text(calendar.monthSymbols[currentMonth - 1] + " \(currentYear)")
.frame(width: 150)
Button(action: {
if currentMonth < 12 {
currentMonth += 1
} else {
currentYear += 1
currentMonth = 1
}
}, label: {
Image(systemName: "chevron.right")
})
// :Month picker
Spacer()
Button("Today") {
goToToday()
}.foregroundColor(.blue)
}
HStack(spacing: 0) {
ForEach(calendar.veryShortWeekdaySymbols) { label in
Text(label)
.foregroundColor(.black)
.frame(width: itemSize, height: itemSize, alignment: .center)
}
}//: HStack
// The calendar values
let firstDayOfMonth = calendar.firstDay(ofMonth: currentMonth,
year: currentYear)
let firstVisibleDay = calendar.firstDayInAWeekContaining(date: firstDayOfMonth)
ForEach((0..<weeksCount)) { row in
HStack(spacing: 0) {
ForEach((0..<calendar.veryShortWeekdaySymbols.count)) { column in
let currentDayOffset = row * calendar.veryShortWeekdaySymbols.count + column
let date = firstVisibleDay.adding(days: currentDayOffset)
DateCellView(date: date,
selectedDay: selectedDay,
appointments: appointmentsByDay[date.dayKey] ?? [],
itemSize: itemSize,
currentMonth: currentMonth)
.onTapGesture {
selectedDay = date
}
}
}
}
}//: VStack
}//: GeometryReader
}
}
struct DateCellView: View {
let date: Date
let selectedDay: Date?
let appointments: [Appointment]
let itemSize: CGFloat
let currentMonth: Int
private let calendar: Calendar = .current
var textColor: Color {
if calendar.component(.month, from: date) == currentMonth {
// Current month color
return .black
} else {
return .gray2
}
}
var isSelected: Bool {
guard let selectedDay = selectedDay else {
return false
}
return calendar.isDate(date, inSameDayAs: selectedDay)
}
var body: some View {
let label = "\(Calendar.current.component(.day, from: date))"
let isSelected = self.isSelected
let tintColor = isSelected ? .white : textColor
let appointmentsColor: Color = isSelected ? .white : .blue
ZStack {
if isSelected {
Circle()
.foregroundColor(.blue)
}
Text(label)
.foregroundColor(tintColor)
HStack {
ForEach(0..<appointments.count) { _ in
Circle()
.frame(width: 6, height: 6.screenScaled)
.foregroundColor(appointmentsColor)
}
}.offset(y: 15.screenScaled)
}.frame(width: itemSize, height: itemSize, alignment: .center)
}
}
import SwiftUI
protocol CalendarPickable: Codable {
var startTime: Date { get }
}
extension CalendarPickable {
var id: UInt { startTime.dayKey }
}
struct Appointment: CalendarPickable {
let startTime: Date
}
struct TimeSlot: CalendarPickable {
let startTime: Date
}
extension Calendar {
func isDateWorkday(date: Date) -> Bool {
if isDateInWeekend(date) { return false }
// TODO: check for holidays
return true
}
}
struct CreateAppointmentsView: View {
let appointments: [Appointment] = [
Appointment(startTime: Date.date(day: 2, month: 12, year: 2020)),
Appointment(startTime: Date.date(day: 2, month: 12, year: 2020)),
Appointment(startTime: Date.date(day: 7, month: 12, year: 2020))
]
private let timeSlotDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
return formatter
}()
@State var selectedDay: Date?
let appointmentsByDay: [UInt: [Appointment]]
init() {
var appointmentsByDay = [UInt: [Appointment]]()
for appointment in appointments {
let key = appointment.startTime.dayKey
var appointsmentsForKey = appointmentsByDay[key] ?? []
appointsmentsForKey.append(appointment)
appointmentsByDay[key] = appointsmentsForKey
}
self.appointmentsByDay = appointmentsByDay
}
var body: some View {
VStack {
CalendarView(selectedDay: $selectedDay, appointmentsByDay: appointmentsByDay)
.padding()
if let selectedDay = selectedDay {
let (pickableSlots, availableSlots) = pickables(date: selectedDay)
Text("\(availableSlots) AVAILABLE TIMESLOTS")
let columnsCount = 3
let rowsCount = pickableSlots.count / columnsCount
ScrollView {
VStack(spacing: 20) {
ForEach(0..<rowsCount) { row in
HStack {
ForEach(0..<columnsCount) { column in
let index = row * columnsCount + column
if index < pickableSlots.count {
let slot = pickableSlots[index]
let isAvailable = slot is TimeSlot
let timeLabel = label(slot: slot)
Button(timeLabel) {
print("\(timeLabel) selected")
}.buttonStyle(
isAvailable ?
// TODO: create a new style for this component
ButtonStyles.filterSelected
:
ButtonStyles.filterUnselected
).disabled(!isAvailable)
.frame(width: 100)
} else {
EmptyView()
}
}//: Columns
}
}//: Rows
}
}
}
Button("Confirm time") {
print("Confirmed!")
}
}
}
func label(slot: CalendarPickable) -> String {
return timeSlotDateFormatter.string(from: slot.startTime)
}
func pickables(date: Date) -> ([CalendarPickable], Int) {
let slots = timeSlots(date: date)
var appointments = appointmentsByDay[date.dayKey] ?? []
var pickables = [CalendarPickable]()
var availableSlots = 0
for slot in slots {
if let index = appointments.firstIndex(where: { a in
return Calendar.current.isDate(a.startTime, theSameAs: slot.startTime, inComponents: [.hour, .minute])
}) {
pickables.append(appointments[index])
appointments.remove(at: index)
} else {
pickables.append(slot)
availableSlots += 1
}
}
return (pickables, availableSlots)
}
func timeSlots(date: Date) -> [TimeSlot] {
guard Calendar.current.isDateWorkday(date: date) else { return [] }
var components = Calendar.current.dateComponents([.day, .month, .year], from: date)
// Start of the day
components.hour = 8
components.minute = 0
let forenoonSlots = timeSlots(components: components, hours: 4)
// Afternoon
components.hour = 13
components.minute = 0
let afternoonSlots = timeSlots(components: components, hours: 4)
return forenoonSlots + afternoonSlots
}
func timeSlots(components: DateComponents, hours: Int) -> [TimeSlot] {
let initialDate = Calendar.current.date(from: components)!
var slots = [TimeSlot]()
for halfhour in 0..<hours*2 {
let startTime = initialDate.addingTimeInterval(TimeInterval.secondsInAnHour / 2 * TimeInterval(halfhour))
slots.append(TimeSlot(startTime:startTime))
}
return slots
}
}
struct CreateAppointmentsView_Previews: PreviewProvider {
static var previews: some View {
CreateAppointmentsView()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment