a native ios client for gitbay

client ios swift

https://gitbay.org

gitbay/Views/Discovery/ActivityGraph.swift

101 lines · 3661 bytes

  1import SwiftUI
  2
  3/// The contribution calendar: 53 weeks of columns, Sunday at the top,
  4/// ending on the current week  the same grid the web draws.
  5struct ActivityGraph: View {
  6
  7    let days: [ProfileViewModel.Profile.Day]
  8
  9    private static let calendar: Calendar = {
 10        var calendar = Calendar(identifier: .gregorian)
 11        calendar.timeZone = TimeZone(identifier: "UTC")!
 12        return calendar
 13    }()
 14
 15    private static let formatter: DateFormatter = {
 16        let formatter = DateFormatter()
 17        formatter.calendar = calendar
 18        formatter.timeZone = calendar.timeZone
 19        formatter.dateFormat = "yyyy-MM-dd"
 20        return formatter
 21    }()
 22
 23    /// Weeks of 7 days; nil is a day outside the window (before the start
 24    /// or after today), drawn as empty space rather than a zero cell.
 25    private var weeks: [[Cell?]] {
 26        let counts = Dictionary(days.map { ($0.date, $0.count) }, uniquingKeysWith: +)
 27        let today = Self.calendar.startOfDay(for: Date())
 28        // End on the Saturday of the current week, matching the server's
 29        // window so both surfaces show the same year.
 30        let weekday = Self.calendar.component(.weekday, from: today) // 1 = Sunday
 31        guard let end = Self.calendar.date(byAdding: .day, value: 7 - weekday, to: today),
 32              let start = Self.calendar.date(byAdding: .day, value: -53 * 7 + 1, to: end) else {
 33            return []
 34        }
 35
 36        var result: [[Cell?]] = []
 37        var cursor = start
 38        while cursor <= end {
 39            var week: [Cell?] = []
 40            for _ in 0..<7 {
 41                if cursor > today {
 42                    week.append(nil)
 43                } else {
 44                    let key = Self.formatter.string(from: cursor)
 45                    week.append(Cell(date: key, count: counts[key] ?? 0))
 46                }
 47                cursor = Self.calendar.date(byAdding: .day, value: 1, to: cursor) ?? cursor
 48            }
 49            result.append(week)
 50        }
 51        return result
 52    }
 53
 54    private struct Cell: Hashable {
 55        let date: String
 56        let count: Int
 57
 58        /// The same five buckets activityLevel uses server-side.
 59        var level: Int {
 60            switch count {
 61            case 0: 0
 62            case 1...2: 1
 63            case 3...5: 2
 64            case 6...9: 3
 65            default: 4
 66            }
 67        }
 68    }
 69
 70    var body: some View {
 71        ScrollView(.horizontal, showsIndicators: false) {
 72            HStack(alignment: .top, spacing: 3) {
 73                ForEach(Array(weeks.enumerated()), id: \.offset) { _, week in
 74                    VStack(spacing: 3) {
 75                        ForEach(Array(week.enumerated()), id: \.offset) { _, cell in
 76                            RoundedRectangle(cornerRadius: 2)
 77                                .fill(color(for: cell))
 78                                .frame(width: 11, height: 11)
 79                        }
 80                    }
 81                }
 82            }
 83            .padding(.vertical, 4)
 84        }
 85        .defaultScrollAnchor(.trailing)
 86    }
 87
 88    /// The stylesheet's ramp: an empty day is --faint, and the rest mix
 89    /// --accent toward --bg at 25/50/75/100 percent. A day outside the
 90    /// window draws nothing at all.
 91    private func color(for cell: Cell?) -> Color {
 92        guard let cell else { return .clear }
 93        return switch cell.level {
 94        case 0: Color.gbFaint
 95        case 1: Color.gbAccent.mix(with: .gbBackground, by: 0.75)
 96        case 2: Color.gbAccent.mix(with: .gbBackground, by: 0.50)
 97        case 3: Color.gbAccent.mix(with: .gbBackground, by: 0.25)
 98        default: Color.gbAccent
 99        }
100    }
101}