fixed iOS widget layout
This commit is contained in:
+4
-1
@@ -644,7 +644,10 @@ object WidgetRenderer {
|
|||||||
WidgetLessonStatus.TEACHER_CHANGED -> R.drawable.widget_lesson_block_teacher_changed
|
WidgetLessonStatus.TEACHER_CHANGED -> R.drawable.widget_lesson_block_teacher_changed
|
||||||
WidgetLessonStatus.PAST -> R.drawable.widget_lesson_block_past
|
WidgetLessonStatus.PAST -> R.drawable.widget_lesson_block_past
|
||||||
WidgetLessonStatus.EVENT -> R.drawable.widget_lesson_block_event_orange
|
WidgetLessonStatus.EVENT -> R.drawable.widget_lesson_block_event_orange
|
||||||
WidgetLessonStatus.ONGOING -> R.drawable.widget_lesson_block_ongoing
|
// ONGOING collapses into REGULAR — widgets only refresh every
|
||||||
|
// ~30min so "the current lesson" is stale most of the time and
|
||||||
|
// the visual highlight would mislead more than help.
|
||||||
|
WidgetLessonStatus.ONGOING,
|
||||||
WidgetLessonStatus.REGULAR -> R.drawable.widget_lesson_block_regular
|
WidgetLessonStatus.REGULAR -> R.drawable.widget_lesson_block_regular
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,8 +39,11 @@ func realMinutesToVirtual(_ realMin: Int, periods: [WidgetPeriod]) -> CGFloat {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let BLOCK_SHOW_ROOM_MIN: CGFloat = 18
|
// Below these tile heights the secondary stack collapses progressively:
|
||||||
let BLOCK_SHOW_TEACHER_SEPARATE_MIN: CGFloat = 30
|
// shorter than ROOM_MIN drops everything, between ROOM_MIN and
|
||||||
|
// TEACHER_SEPARATE_MIN keeps the room but drops the teacher.
|
||||||
|
let BLOCK_SHOW_ROOM_MIN: CGFloat = 13
|
||||||
|
let BLOCK_SHOW_TEACHER_SEPARATE_MIN: CGFloat = 20
|
||||||
|
|
||||||
let MIN_SUBJECT_FONT: CGFloat = 9
|
let MIN_SUBJECT_FONT: CGFloat = 9
|
||||||
let MAX_SUBJECT_FONT: CGFloat = 14
|
let MAX_SUBJECT_FONT: CGFloat = 14
|
||||||
@@ -70,7 +73,7 @@ struct TimetableDayView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func content(data: WidgetTimetableData) -> some View {
|
private func content(data: WidgetTimetableData) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
header(data: data)
|
header(data: data)
|
||||||
if data.isHoliday {
|
if data.isHoliday {
|
||||||
emptyState(text: data.holidayName ?? "Ferien")
|
emptyState(text: data.holidayName ?? "Ferien")
|
||||||
@@ -96,14 +99,18 @@ struct TimetableDayView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func header(data: WidgetTimetableData) -> some View {
|
private func header(data: WidgetTimetableData) -> some View {
|
||||||
HStack {
|
HStack(spacing: 4) {
|
||||||
Text(dayLabel(for: data.anchorDate))
|
Text(dayLabel(for: data.anchorDate))
|
||||||
.font(.system(size: 14, weight: .semibold))
|
.font(.system(size: 13, weight: .semibold))
|
||||||
.foregroundStyle(palette.textPrimary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
Spacer()
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
|
Spacer(minLength: 4)
|
||||||
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 9))
|
||||||
.foregroundStyle(palette.textSecondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.7)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,6 +219,14 @@ struct TimeGridView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var gridLines: some View {
|
private var gridLines: some View {
|
||||||
|
// `.frame(height: totalHeight, alignment: .top)` — without the
|
||||||
|
// explicit `.top`, SwiftUI's frame modifier defaults to `.center`.
|
||||||
|
// Every Rectangle here has layout-y=0 (the `.offset()`s shift the
|
||||||
|
// rendered position only, not layout), so the inner ZStack's
|
||||||
|
// natural height is just 1pt; a 1pt ZStack centred in a 300pt
|
||||||
|
// frame anchors at the middle, and every line then renders
|
||||||
|
// relative to that midpoint — the grid visually starts halfway
|
||||||
|
// down the column and the later periods clip past the bottom.
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
||||||
Rectangle()
|
Rectangle()
|
||||||
@@ -220,10 +235,11 @@ struct TimeGridView: View {
|
|||||||
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: totalHeight)
|
.frame(height: totalHeight, alignment: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var breakBlocks: some View {
|
private var breakBlocks: some View {
|
||||||
|
// Same `.center`-vs-`.top` bug as gridLines — see comment above.
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ForEach(0..<max(0, periods.count - 1), id: \.self) { i in
|
ForEach(0..<max(0, periods.count - 1), id: \.self) { i in
|
||||||
let curr = periods[i]
|
let curr = periods[i]
|
||||||
@@ -238,7 +254,7 @@ struct TimeGridView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: totalHeight)
|
.frame(height: totalHeight, alignment: .top)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func formatHm(_ minutes: Int) -> String {
|
private func formatHm(_ minutes: Int) -> String {
|
||||||
@@ -357,8 +373,12 @@ struct TimeGridView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch lesson.status {
|
switch lesson.status {
|
||||||
case .regular, .past: return Color(red: 153/255.0, green: 51/255.0, blue: 51/255.0)
|
// `.ongoing` deliberately collapses into the regular red — the
|
||||||
case .ongoing: return Color(red: 200/255.0, green: 51/255.0, blue: 51/255.0)
|
// widget timeline only ticks every ~30min, so highlighting the
|
||||||
|
// "current" lesson would be stale most of the day and misleads
|
||||||
|
// more than it helps.
|
||||||
|
case .regular, .past, .ongoing:
|
||||||
|
return Color(red: 153/255.0, green: 51/255.0, blue: 51/255.0)
|
||||||
case .cancelled: return .black
|
case .cancelled: return .black
|
||||||
case .irregular: return Color(red: 143/255.0, green: 25/255.0, blue: 179/255.0)
|
case .irregular: return Color(red: 143/255.0, green: 25/255.0, blue: 179/255.0)
|
||||||
case .teacherChanged: return Color(red: 41/255.0, green: 99/255.0, blue: 155/255.0)
|
case .teacherChanged: return Color(red: 41/255.0, green: 99/255.0, blue: 155/255.0)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct TimetableWeekView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func content(data: WidgetTimetableData) -> some View {
|
private func content(data: WidgetTimetableData) -> some View {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
header(data: data)
|
header(data: data)
|
||||||
dayHeaderRow(data: data)
|
dayHeaderRow(data: data)
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
@@ -30,7 +30,10 @@ struct TimetableWeekView: View {
|
|||||||
min(MAX_HOUR_HEIGHT, geo.size.height / max(totalMin, 60) * 60)
|
min(MAX_HOUR_HEIGHT, geo.size.height / max(totalMin, 60) * 60)
|
||||||
)
|
)
|
||||||
let dayColumnWidth = (geo.size.width - 28 - 4) / 5
|
let dayColumnWidth = (geo.size.width - 28 - 4) / 5
|
||||||
let subjectOnly = dayColumnWidth < 70
|
// 45pt is the minimum where a 7pt-font room (e.g. "F2.04")
|
||||||
|
// plus the subject fits without `minimumScaleFactor`
|
||||||
|
// shrinking the subject into illegibility.
|
||||||
|
let subjectOnly = dayColumnWidth < 45
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
timeLabelsColumn(hourHeight: hourHeight, periods: data.periods)
|
timeLabelsColumn(hourHeight: hourHeight, periods: data.periods)
|
||||||
.frame(width: 28, alignment: .topTrailing)
|
.frame(width: 28, alignment: .topTrailing)
|
||||||
@@ -62,12 +65,15 @@ struct TimetableWeekView: View {
|
|||||||
let endDate = cal.date(byAdding: .day, value: 4, to: data.anchorDate) ?? data.anchorDate
|
let endDate = cal.date(byAdding: .day, value: 4, to: data.anchorDate) ?? data.anchorDate
|
||||||
return HStack {
|
return HStack {
|
||||||
Text("KW \(week) · \(shortDate(data.anchorDate))–\(shortDate(endDate))")
|
Text("KW \(week) · \(shortDate(data.anchorDate))–\(shortDate(endDate))")
|
||||||
.font(.system(size: 13, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundStyle(palette.textPrimary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(0.8)
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 9))
|
||||||
.foregroundStyle(palette.textSecondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,24 +91,30 @@ struct TimetableWeekView: View {
|
|||||||
columnDivider
|
columnDivider
|
||||||
ForEach(0..<5, id: \.self) { offset in
|
ForEach(0..<5, id: \.self) { offset in
|
||||||
let day = cal.date(byAdding: .day, value: offset, to: data.anchorDate) ?? data.anchorDate
|
let day = cal.date(byAdding: .day, value: offset, to: data.anchorDate) ?? data.anchorDate
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: -1) {
|
||||||
Text(weekday(for: day))
|
Text(weekday(for: day))
|
||||||
.font(.system(size: 11, weight: .bold))
|
.font(.system(size: 10, weight: .bold))
|
||||||
.foregroundStyle(palette.textPrimary)
|
.foregroundStyle(palette.textPrimary)
|
||||||
Text(shortDate(day))
|
Text(shortDate(day))
|
||||||
.font(.system(size: 9))
|
.font(.system(size: 8))
|
||||||
.foregroundStyle(palette.textSecondary)
|
.foregroundStyle(palette.textSecondary)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
if offset < 4 { columnDivider }
|
if offset < 4 { columnDivider }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, minHeight: 26, maxHeight: 26)
|
// 20pt fits 10pt weekday + 8pt date with tight -1pt spacing.
|
||||||
|
.frame(maxWidth: .infinity, minHeight: 20, maxHeight: 20)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func timeLabelsColumn(hourHeight: CGFloat, periods: [WidgetPeriod]) -> some View {
|
private func timeLabelsColumn(hourHeight: CGFloat, periods: [WidgetPeriod]) -> some View {
|
||||||
let totalMin = periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
|
let totalMin = periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
|
||||||
let totalHeight = CGFloat(totalMin) * hourHeight / 60.0
|
let totalHeight = CGFloat(totalMin) * hourHeight / 60.0
|
||||||
|
// Mirrors TimeGridView.compactLabels — below 26pt/hour a stacked
|
||||||
|
// time+name no longer fits in one period slot and the two labels
|
||||||
|
// visually clip into the next slot. Drop to a single bold period
|
||||||
|
// number until there's room to read both lines.
|
||||||
|
let compact = hourHeight < 26
|
||||||
return ZStack(alignment: .topTrailing) {
|
return ZStack(alignment: .topTrailing) {
|
||||||
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
||||||
Rectangle()
|
Rectangle()
|
||||||
@@ -112,15 +124,23 @@ struct TimetableWeekView: View {
|
|||||||
}
|
}
|
||||||
ForEach(periods, id: \.startMinutes) { period in
|
ForEach(periods, id: \.startMinutes) { period in
|
||||||
VStack(alignment: .trailing, spacing: -2) {
|
VStack(alignment: .trailing, spacing: -2) {
|
||||||
Text(String(format: "%02d:%02d", period.startMinutes / 60, period.startMinutes % 60))
|
if compact {
|
||||||
.font(.system(size: 8))
|
Text("\(period.name).")
|
||||||
.foregroundStyle(palette.textPrimary)
|
.font(.system(size: 9, weight: .bold))
|
||||||
.lineLimit(1)
|
.foregroundStyle(palette.textPrimary)
|
||||||
Text("\(period.name).")
|
.lineLimit(1)
|
||||||
.font(.system(size: 6, weight: .bold))
|
} else {
|
||||||
.foregroundStyle(palette.textSecondary)
|
Text(String(format: "%02d:%02d", period.startMinutes / 60, period.startMinutes % 60))
|
||||||
.lineLimit(1)
|
.font(.system(size: 9))
|
||||||
|
.foregroundStyle(palette.textPrimary)
|
||||||
|
.lineLimit(1)
|
||||||
|
Text("\(period.name).")
|
||||||
|
.font(.system(size: 7, weight: .bold))
|
||||||
|
.foregroundStyle(palette.textSecondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.padding(.trailing, 4)
|
||||||
.offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
|
.offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,11 @@ struct TimetableDayWidget: Widget {
|
|||||||
}
|
}
|
||||||
.configurationDisplayName("Marianum · Heute")
|
.configurationDisplayName("Marianum · Heute")
|
||||||
.description("Stundenplan und Vertretungen für den anstehenden Schultag.")
|
.description("Stundenplan und Vertretungen für den anstehenden Schultag.")
|
||||||
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
|
// Only .systemLarge: iOS gives us no tall-portrait family, so the
|
||||||
|
// 162×162 .systemSmall can't fit a readable 11-period day grid,
|
||||||
|
// and .systemMedium is short-wide which crunches the labels.
|
||||||
|
// Single useful size beats three crippled ones.
|
||||||
|
.supportedFamilies([.systemLarge])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +60,10 @@ struct TimetableWeekWidget: Widget {
|
|||||||
}
|
}
|
||||||
.configurationDisplayName("Marianum · Woche")
|
.configurationDisplayName("Marianum · Woche")
|
||||||
.description("Stundenplan und Vertretungen für die ganze Schulwoche.")
|
.description("Stundenplan und Vertretungen für die ganze Schulwoche.")
|
||||||
.supportedFamilies([.systemMedium, .systemLarge, .systemExtraLarge])
|
// .systemMedium is dropped — 5 day columns plus a time-label column
|
||||||
|
// in a 4×2 cell-strip yields per-column widths well below the
|
||||||
|
// subject-only threshold and the labels in the time sidebar overlap.
|
||||||
|
.supportedFamilies([.systemLarge, .systemExtraLarge])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user