Search code examples
swiftswiftuicalendar

Create a daily timeline calendar in SwiftUI


I’m looking for advice on how to approach building a SwiftUI daily calendar list as I'm struggling to think how to even approach this.

This is the type of list I’m trying to create: image

I can create the VStack to loop over the hours in the day vertically and then the dividing lines too - which looks like the Apple Calendar app view: blank list

What I can’t figure out is how to align the calendar entries to the time, and then spanning it over multiple hours if it is long event.

I receive the event data from MSGraph with a startDate and endDate.

I don't require dragging ability of the events, only displaying them in the time slots.

My aim is to build it all myself, though I know there are probably packages that could help. It's just a small part of the app that I didn't want to overload with calendar functionalities when I wanted to have just a viewer.

This is where I am at, but dont know how to add the events into the list:

struct ContentView: View {
 var body: some View {
  ScrollView {
   VStack(alignment: .leading) {
    ForEach(8..<17) { index in
      Text("\(index)")
      Divider()
       .offset(y: -22)
       .padding(.leading, 30)
      Spacer()
     }
    }
   }
   .frame(maxWidth: .infinity, alignment: .leading)
   .padding()
  }
}

Solution

  • I would add the single events over the hours grid as overlay or in an ZStack. You have to calculate an offset based on start date.

    Here would be a sample implementation:

    enter image description here

    struct Event: Identifiable {
        let id = UUID()
        var startDate: Date
        var endDate: Date
        var title: String
    }
    
    
    struct ContentView: View {
        
        let date: Date = dateFrom(9, 5, 2023)
        
        let events: [Event] = [
            Event(startDate: dateFrom(9,5,2023,7,0), endDate: dateFrom(9,5,2023,8,0), 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 hourHeight = 50.0
        
        var body: some View {
            VStack(alignment: .leading) {
                
                // Date headline
                HStack {
                    Text(date.formatted(.dateTime.day().month()))
                        .bold()
                    Text(date.formatted(.dateTime.year()))
                }
                .font(.title)
                Text(date.formatted(.dateTime.weekday(.wide)))
                
                ScrollView {
                    ZStack(alignment: .topLeading) {
                        
                        VStack(alignment: .leading, spacing: 0) {
                            ForEach(7..<19) { hour in
                                HStack {
                                    Text("\(hour)")
                                        .font(.caption)
                                        .frame(width: 20, alignment: .trailing)
                                    Color.gray
                                        .frame(height: 1)
                                }
                                .frame(height: hourHeight)
                            }
                        }
                        
                        ForEach(events) { event in
                            eventCell(event)
                        }
                    }
                }
            }
            .padding()
        }
        
        func eventCell(_ event: Event) -> some View {
            
            let duration = event.endDate.timeIntervalSince(event.startDate)
            let height = duration / 60 / 60 * hourHeight
            
            let calendar = Calendar.current
            let hour = calendar.component(.hour, from: event.startDate)
            let minute = calendar.component(.minute, from: event.startDate)
            let offset = Double(hour-7) * (hourHeight)
    //                      + Double(minute)/60 ) * hourHeight
            
            print(hour, minute, Double(hour-7) + Double(minute)/60 )
    
            return VStack(alignment: .leading) {
                Text(event.startDate.formatted(.dateTime.hour().minute()))
                Text(event.title).bold()
            }
            .font(.caption)
            .frame(maxWidth: .infinity, alignment: .leading)
            .padding(4)
            .frame(height: height, alignment: .top)
            .background(
                RoundedRectangle(cornerRadius: 8)
                    .fill(.teal).opacity(0.5)
            )
            .padding(.trailing, 30)
            .offset(x: 30, y: offset + 24)
    
        }
        
    }
    
    
    func dateFrom(_ day: Int, _ month: Int, _ year: Int, _ hour: Int = 0, _ minute: Int = 0) -> Date {
        let calendar = Calendar.current
        let dateComponents = DateComponents(year: year, month: month, day: day, hour: hour, minute: minute)
        return calendar.date(from: dateComponents) ?? .now
    }