0

I am trying to create a UI like the below. I have a startDate and endDate (e.g. 1:55am and 11:35am). How can I proportionally (using Geometry reader) find each hour in between two dates and plot them as is done here? I am thinking at some point I need to use seconds or timeIntervalSince perhaps, but can't quite get my head around how best to go about it?

enter image description here

my code so far which gets the hours correctly, but I need to space them out proportionally according to their times:

What my code looks like: enter image description here

struct AsleepTimeView: View {
    
    @EnvironmentObject var dataStore: DataStore
    
    static let sleepTimeFormat: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateStyle = .none
        formatter.timeStyle = .short
        return formatter
    }()
    
    
    var body: some View {
        Color.clear.overlay(
            GeometryReader { geometry in
                if let mostRecentSleepDay = dataStore.pastSevenSleepDays?.last, let firstSleepSpan = mostRecentSleepDay.sleepSpans.first, let lastSleepSpan = mostRecentSleepDay.sleepSpans.last {
                    VStack(alignment: .center) {
                        HStack {
                            Text("\(firstSleepSpan.startDate, formatter: Self.sleepTimeFormat) ")
                                .font(.footnote)
                            Spacer()
                            Text("\(lastSleepSpan.endDate, formatter: Self.sleepTimeFormat)")
                                .font(.footnote)
                        }
                        HStack(spacing: 3) {
                            
                            ForEach(dataStore.sleepOrAwakeSpans) { sleepOrAwakeSpan in
                                
                                RoundedRectangle(cornerRadius: 5)
                                    .frame(width: getWidthForRoundedRectangle(proxy: geometry, spacing: 3, seconds: sleepOrAwakeSpan.seconds, sleepOrAwakeSpans: athlyticDataStore.sleepOrAwakeSpans), height: 10)
                                    
                                    .foregroundColor(sleepOrAwakeSpan.asleep == false ? TrackerConstants.scaleLevel5Color : TrackerConstants.scaleLevel8Color)
                                
                            }
                        }
                        HStack {
                            ForEach(getHoursBetweenTwoDates(startDate: firstSleepSpan.startDate, endDate: lastSleepSpan.endDate).map {  Calendar.current.component(.hour, from: $0) }, id: \.self) { hour in
                                HStack {
                                    Text("\(hour)")
                                        .font(.footnote)
                                    Spacer()
                                }
                            }
                        }
                        HStack {
                            HStack {
                                Circle()
                                    .foregroundColor(TrackerConstants.scaleLevel8Color)
                                    .frame(width: 10, height: 10)
                                Text("Asleep")
                                    .font(.footnote)
                            }
                            HStack {
                                Circle()
                                    .foregroundColor(TrackerConstants.scaleLevel5Color)
                                    .frame(width: 10, height: 10)
                                Text("Awake")
                                    .font(.footnote)
                            }
                            Spacer()
                        }
                    }
                }
 
            })     // end of overlay
    }
    
    //helper
    private func getWidthForRoundedRectangle(proxy: GeometryProxy, spacing: Int, seconds: TimeInterval, sleepOrAwakeSpans: [SleepOrAwakeSpan]) -> CGFloat {
        
        let totalSpace = (sleepOrAwakeSpans.count - 1) * spacing
        let totalSleepTime = sleepOrAwakeSpans.map { $0.endTime.timeIntervalSince($0.startTime) }.reduce(0, +)
        
        guard totalSleepTime > 0 else { return 0}
        
        let width =  (proxy.size.width - CGFloat(totalSpace)) * CGFloat(seconds / totalSleepTime)
        return width
    }
    
    func datesRange(from: Date, to: Date, component: Calendar.Component) -> [Date] {
        // in case of the "from" date is more than "to" date,
        // it should returns an empty array:
        if from > to { return [Date]() }
        
        var tempDate = from
        var array = [tempDate]
        
        while tempDate < to {
            tempDate = Calendar.current.date(byAdding: component, value: 1, to: tempDate)!
            array.append(tempDate)
        }
        
        return array
    }
    
    func getHoursBetweenTwoDates(startDate: Date, endDate: Date) -> [Date] {
        
        var finalArrayOfHours = [Date]()
        
        guard endDate > startDate else { return finalArrayOfHours }
        
        let arrayOfHours = datesRange(from: startDate, to: endDate, component: .hour)
        
        for date in arrayOfHours {
            let hour = date.nearestHour()
            finalArrayOfHours.append(hour)
        }
        
        return finalArrayOfHours
        
    }
    
}

extension Date {
    func nearestHour() -> Date {
        return Date(timeIntervalSinceReferenceDate:
                        (timeIntervalSinceReferenceDate / 3600.0).rounded(.toNearestOrEven) * 3600.0)
    }
}
GarySabo
  • 5,806
  • 5
  • 49
  • 124
  • the issue is not related to GeometryReader, GeometryReader is like a pen, and we need data to draw something with that pen! so you need work and solve problem in your logic after making your data ready then that is time to draw or use GeometryReader. – ios coder Apr 06 '21 at 12:13
  • @swiftPunk thanks, I edited my question to what I've got so far based on Duncan's answer and some additional research but I still need to space the hours proportionally so they align to the data above. – GarySabo Apr 06 '21 at 17:52

1 Answers1

0

There are a whole suite of methods in the Calendar class to help with what you're trying to do.

Off the top of my head, Id say you'd do something like the following:

In a loop, use date(byAdding:to:wrappingComponents:) to add an hour at a time to your startDate. If the result is ≤ endDate, use component(_:from:) to get the hour of the newly calculated date.

Duncan C
  • 128,072
  • 22
  • 173
  • 272
  • thanks! I edited my question with my stab at getting the hours based on what you said, however I still need to align the hours so they are proportional to the time span so that they align to the data above. – GarySabo Apr 06 '21 at 17:53