Skip to content

Instantly share code, notes, and snippets.

@markbattistella
Last active June 21, 2023 12:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markbattistella/621cbe996805a7bd14f76a239387e866 to your computer and use it in GitHub Desktop.
Save markbattistella/621cbe996805a7bd14f76a239387e866 to your computer and use it in GitHub Desktop.
Show a list of all the events in a daily view
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