crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

CalendarDayCell.swift (3470B)


      1 import SwiftUI
      2 
      3 struct CalendarDayCell: View {
      4     let date: Date
      5     let dayNumber: Int
      6     let isEnabled: Bool
      7     let isToday: Bool
      8     let isSelected: Bool
      9     let isInLibrary: Bool
     10     let onTap: () -> Void
     11 
     12     var body: some View {
     13         Button(action: onTap) {
     14             Text("\(dayNumber)")
     15                 .font(.body)
     16                 .fontWeight(isToday || isSelected ? .bold : .regular)
     17                 // A floor rather than a fixed height: the row lets the cell
     18                 // stretch when there is room and compresses it towards 36 when
     19                 // six weeks have to fit a fixed-height sheet. The full column
     20                 // width keeps the tap target comfortable either way.
     21                 .frame(maxWidth: .infinity, minHeight: 36, maxHeight: .infinity)
     22                 .foregroundStyle(foregroundColor)
     23                 .background {
     24                     ZStack {
     25                         // A day already in the library owns the fill, so the
     26                         // grey circle is never displaced by selection state.
     27                         if isInLibrary {
     28                             Circle()
     29                                 .fill(Color(.systemFill))
     30                                 .frame(width: 36, height: 36)
     31                         } else if isSelected {
     32                             Circle()
     33                                 .fill(Color.accentColor)
     34                                 .frame(width: 36, height: 36)
     35                         } else if isToday {
     36                             Circle()
     37                                 .fill(Color.accentColor.opacity(0.15))
     38                                 .frame(width: 36, height: 36)
     39                         }
     40                         // With the fill taken, selection moves to a ring drawn
     41                         // inside the same 36pt footprint — an outset ring would
     42                         // sit on the cell's edge in the narrowest column.
     43                         if isInLibrary, isSelected || isToday {
     44                             Circle()
     45                                 .strokeBorder(
     46                                     isSelected ? Color.accentColor : Color.accentColor.opacity(0.4),
     47                                     lineWidth: 2.5
     48                                 )
     49                                 .frame(width: 36, height: 36)
     50                         }
     51                     }
     52                 }
     53         }
     54         .buttonStyle(.plain)
     55         .disabled(!isEnabled)
     56         .accessibilityLabel(accessibilityLabel)
     57         .accessibilityValue(accessibilityValue)
     58         .accessibilityHint(isEnabled ? "Selects this puzzle date" : "")
     59         .accessibilityAddTraits(isSelected ? .isSelected : [])
     60     }
     61 
     62     private var foregroundColor: Color {
     63         // White reads against the accent fill, but a selected library day is
     64         // filled grey and keeps the normal label colour.
     65         if isSelected, !isInLibrary { return .white }
     66         return isEnabled ? .primary : .secondary.opacity(0.4)
     67     }
     68 
     69     private var accessibilityLabel: String {
     70         date.formatted(.dateTime.weekday(.wide).month(.wide).day().year())
     71     }
     72 
     73     private var accessibilityValue: String {
     74         var parts: [String] = []
     75         if isToday { parts.append("Today") }
     76         if isSelected { parts.append("Selected") }
     77         if isInLibrary { parts.append("In your library") }
     78         if !isEnabled { parts.append("Unavailable") }
     79         return parts.joined(separator: ", ")
     80     }
     81 }