-
-
Save markbattistella/621cbe996805a7bd14f76a239387e866 to your computer and use it in GitHub Desktop.
Show a list of all the events in a daily view
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 | |
/// A view representing the main content of the app. | |
struct MainView: View { | |
var body: some View { | |
/// Display the calendar view with the specified parameters. | |
CalendarView( | |
startHour: 0, | |
endHour: 23, | |
calendarHeight: 600, | |
events: Event.mockEvents, | |
use24HourFormat: false | |
) | |
} | |
} | |
/// A struct representing an event. | |
struct Event: Identifiable, Hashable { | |
let id: UUID = UUID() | |
let title: String | |
let startDateTime: Date | |
let endDateTime: Date | |
} | |
extension Event { | |
/// An array of mock events for testing purposes. | |
static let mockEvents: [Event] = [ | |
Event( | |
title: "Induction", | |
startDateTime: .from("2023-06-20 7:05"), | |
endDateTime: .from("2023-06-20 8:10") | |
), | |
Event( | |
title: "Product meeting", | |
startDateTime: .from("2023-06-20 8:10"), | |
endDateTime: .from("2023-06-20 8:30") | |
), | |
Event( | |
title: "Potential Call", | |
startDateTime: .from("2023-06-20 9:15"), | |
endDateTime: .from("2023-06-20 15:45") | |
), | |
Event( | |
title: "Offsite scope", | |
startDateTime: .from("2023-06-20 12:00"), | |
endDateTime: .from("2023-06-20 13:30") | |
), | |
Event( | |
title: "Presentation", | |
startDateTime: .from("2023-06-20 17:00"), | |
endDateTime: .from("2023-06-20 18:30") | |
) | |
] | |
} | |
/// A view representing the calendar display. | |
struct CalendarView: View { | |
var startHour: Int | |
var endHour: Int | |
let calendarHeight: CGFloat | |
let events: [Event] | |
var use24HourFormat: Bool | |
private let hourLabel: CGSize = .init(width: 38, height: 38) | |
private let offsetPadding: Double = 10 | |
/// The height of each hour in the calendar. | |
private var hourHeight: CGFloat { | |
calendarHeight / CGFloat(endHour - startHour + 1) | |
} | |
/// Groups the overlapping events together. | |
private var overlappingEventGroups: [[Event]] { | |
EventProcessor.processEvents(events) | |
} | |
var body: some View { | |
ScrollView(.vertical, showsIndicators: false) { | |
ZStack(alignment: .topLeading) { | |
timeHorizontalLines | |
ForEach(overlappingEventGroups, id: \.self) { overlappingEvents in | |
HStack(alignment: .top, spacing: 0) { | |
ForEach(overlappingEvents) { event in | |
eventCell(for: event) | |
} | |
} | |
} | |
.offset(x: hourLabel.width + offsetPadding) | |
.padding(.trailing, hourLabel.width + offsetPadding) | |
} | |
} | |
.frame(minHeight: calendarHeight, alignment: .bottom) | |
} | |
/// A view displaying the horizontal time lines in the calendar. | |
private var timeHorizontalLines: some View { | |
VStack(spacing: 0) { | |
ForEach(startHour ... endHour, id: \.self) { hour in | |
HStack(spacing: 10) { | |
/// Display the formatted hour label. | |
Text(formattedHour(hour)) | |
.font(.caption2) | |
.monospacedDigit() | |
.frame(width: hourLabel.width, height: hourLabel.height, alignment: .trailing) | |
Rectangle() | |
.fill(.gray.opacity(0.6)) | |
.frame(height: 1) | |
} | |
.foregroundColor(.gray) | |
.frame(height: hourHeight, alignment: .top) | |
} | |
} | |
} | |
/// Formats the hour string based on the 24-hour format setting. | |
/// | |
/// - Parameter hour: The hour value to format. | |
/// - Returns: The formatted hour string. | |
private func formattedHour(_ hour: Int) -> String { | |
if use24HourFormat { | |
return String(format: "%02d:00", hour) | |
} else { | |
switch hour { | |
case 0, 12: | |
return "12 \(hour == 0 ? "am" : "pm")" | |
case 13...23: | |
return "\(hour - 12) pm" | |
default: | |
return "\(hour) am" | |
} | |
} | |
} | |
/// Creates a view representing an event cell in the calendar. | |
/// | |
/// - Parameter event: The event to display. | |
/// - Returns: A view representing the event cell. | |
private func eventCell(for event: Event) -> some View { | |
let offsetPadding: CGFloat = 10 | |
var duration: Double { | |
event.endDateTime.timeIntervalSince(event.startDateTime) | |
} | |
var height: Double { | |
let timeHeight = (duration / 60 / 60) * Double(hourHeight) | |
return timeHeight < 16 ? 16 : timeHeight | |
} | |
let calendar = Calendar.current | |
var hour: Int { | |
calendar.component(.hour, from: event.startDateTime) | |
} | |
var minute: Int { | |
calendar.component(.minute, from: event.startDateTime) | |
} | |
var offset: Double { | |
(Double(hour - startHour) * Double(hourHeight)) + | |
(Double(minute) / 60 * Double(hourHeight)) + | |
offsetPadding | |
} | |
return Text(event.title) | |
.bold() | |
.padding() | |
.frame(maxWidth: .infinity, alignment: .leading) | |
.frame(height: CGFloat(height)) | |
.minimumScaleFactor(0.6) | |
.multilineTextAlignment(.leading) | |
.background( | |
Rectangle() | |
.fill(Color.mint.opacity(0.6)) | |
.padding(1) | |
) | |
.offset(y: CGFloat(offset)) | |
} | |
} | |
/// A helper struct for processing events and grouping overlapping events. | |
fileprivate struct EventProcessor { | |
/// Groups the given events based on overlapping time intervals. | |
/// | |
/// - Parameter events: The events to process. | |
/// - Returns: An array of event groups where each group contains overlapping events. | |
static func processEvents(_ events: [Event]) -> [[Event]] { | |
let sortedEvents = events.sorted { | |
$0.startDateTime < $1.startDateTime | |
} | |
var processedEvents: [[Event]] = [] | |
var currentEvents: [Event] = [] | |
for event in sortedEvents { | |
if let latestEndTimeInCurrentEvents = currentEvents.map({ $0.endDateTime }).max(), | |
event.startDateTime < latestEndTimeInCurrentEvents { | |
currentEvents.append(event) | |
} else { | |
if !currentEvents.isEmpty { | |
processedEvents.append(currentEvents) | |
} | |
currentEvents = [event] | |
} | |
} | |
if !currentEvents.isEmpty { | |
processedEvents.append(currentEvents) | |
} | |
return processedEvents | |
} | |
} | |
extension Date { | |
/// Creates a `Date` object from the given string representation. | |
/// | |
/// - Parameters: | |
/// - dateString: The string representing the date. | |
/// - format: The format of the date string. Default is "yyyy-MM-dd HH:mm". | |
/// - Returns: A `Date` object created from the string representation. | |
static func from(_ dateString: String, format: String = "yyyy-MM-dd HH:mm") -> Date { | |
let dateFormatter = DateFormatter() | |
dateFormatter.dateFormat = format | |
return dateFormatter.date(from: dateString)! | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment