I’m working on a SwiftUI view to display events in a calendar. Currently, the events are displayed vertically, and I have them working fine when there are no overlaps.
However, I’m struggling to figure out how to handle events that overlap in terms of their hours or minutes. I want these overlapping events to be displayed horizontally, side-by-side.
I have already implemented the basic functionality, and I’ve included some sample code to demonstrate the current state of my implementation.
import SwiftUI
struct Event: Identifiable, Decodable {
var id: UUID { .init() }
var startDate: Date
var endDate: Date
var title: String
}
extension Date {
static func dateFrom(_ day: Int, _ month: Int, _ year: Int, _ hour: Int, _ minute: Int) -> Date {
let calendar = Calendar.current
let dateComponents = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
return calendar.date(from: dateComponents) ?? .now
}
}
struct CalendarComponent: View {
var startHour: Int = 9
var endHour: Int = 17
// let events: [Event] // <--- would be passed in
let events: [Event] = [ // <--- mock entries
Event(startDate: .dateFrom(9, 5, 2023, 9, 15), endDate: .dateFrom(9, 5, 2023, 10, 15), title: "Event 1"),
Event(startDate: .dateFrom(9, 5, 2023, 9, 0), endDate: .dateFrom(9, 5, 2023, 10, 0), title: "Event 2"),
Event(startDate: .dateFrom(9, 5, 2023, 11, 0), endDate: .dateFrom(9, 5, 2023, 12, 00), title: "Event 3"),
Event(startDate: .dateFrom(9, 5, 2023, 13, 0), endDate: .dateFrom(9, 5, 2023, 14, 45), title: "Event 4"),
Event(startDate: .dateFrom(9, 5, 2023, 15, 0), endDate: .dateFrom(9, 5, 2023, 15, 45), title: "Event 5")
]
let calendarHeight: CGFloat // total height of calendar
private var hourHeight: CGFloat {
calendarHeight / CGFloat( endHour - startHour + 1)
}
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
ZStack(alignment: .topLeading) {
VStack(spacing: 0) {
ForEach(startHour ... endHour, id: .self) { hour in
HStack(spacing: 10) {
Text("(hour)")
.font(.caption2)
.foregroundColor(.gray)
.monospacedDigit()
.frame(width: 20, height: 20, alignment: .center)
Rectangle()
.fill(.gray.opacity(0.5))
.frame(height: 1)
}
.frame(height: hourHeight, alignment: .top)
}
}
ForEach(events) { event in
eventCell(event, hourHeight: hourHeight)
}
.frame(maxHeight: .infinity, alignment: .top)
.offset(x: 30, y: 10)
}
}
.frame(minHeight: calendarHeight, alignment: .bottom)
}
private func eventCell(_ event: Event, hourHeight: CGFloat) -> some View {
var duration: Double { event.endDate.timeIntervalSince(event.startDate) }
var height: Double { (duration / 60 / 60) * hourHeight }
let calendar = Calendar.current
var hour: Int { calendar.component(.hour, from: event.startDate) }
var minute: Int { calendar.component(.minute, from: event.startDate) }
// hour + minute + padding offset from top
var offset: Double {
((CGFloat(hour - 9) * hourHeight) + (CGFloat(minute / 60) * hourHeight) + 10)
}
return Text(event.title).bold()
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.frame(height: height)
.background(
RoundedRectangle(cornerRadius: 10)
.fill(.red.opacity(0.2))
.padding(.trailing, 30)
)
.offset(y: offset)
}
}
The code produces a calendar view where events are displayed vertically, but I would like them to be split horizontally when there is an overlap. For example, if "Event 1" and "Event 2" overlap, I want them to be displayed side-by-side.
I’ve attached two images to illustrate the current output and the desired outcome:
I’m seeking guidance on how to modify my code to achieve this horizontal splitting of events when there is an overlap. Targeting iOS / iPadOS 15.6 and above.
Any suggestions or insights would be greatly appreciated. Thank you in advance for your help!
2
Answers
So I wanted to expand on valosip's answeras it helped me get to my end result. But I wanted to flesh it out for future users who might want to see some more in-depth workings.
First I was working with this model:
And created these
mockEvents
for testing:I also used this
Date
helper to format string dates into proper dates:The main view - which you can put into your other SwiftUI views looked like this:
The
CalendarView
looked like the below. I added as much comments as I could to help explain what was going on:But the most important part of the whole code, which solved the collisions was this
EventProcessor
function:In the end, I had something that looked like this:
Again, if there is anyone that wants to see or read more of how I got to this place with the help of the StackOverflow answer read it all here
There are a few ways to go about doing this.
Personally I’d update the format of the events. You need to find the overlapping/collision events.
Add an extension to Events to compare one to another, or create a function that compares one Event to another.
These are the simplest cases..
OR
Create a function to group your events:
Update you UI to loop over your grouped events. Replace your
ForEach(events)
with something like this:Below you’ll see a quick output from my sudo code. It’s quick and dirty just to give an idea.