Created
December 6, 2020 21:57
-
-
Save Jxrgxn/ff9f02137604319b2f5f76831fa3b630 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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)! | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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