I tried my hand at my own version of a calendar app. I can change the year displayed in the overview using arrow buttons. The current date is colored red.
However, if I change the year and then switch back to the current date, the highlight is gone. I can't figure out how to change this. Can you help me? Here I show you the code:
import SwiftUI
struct MonthOverviewView: View {
@State private var year: Int = Calendar.current.component(.year, from: Date()) // Aktuelles Jahr
@State private var scrollProxy: ScrollViewProxy? = nil
@State private var hasScrolledToToday = false // Verhindert mehrfaches Scrollen beim Laden der View
private let calendar = Calendar.current
private let daysOfWeek = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
private let currentDay = Calendar.current.component(.day, from: Date())
private let currentMonth = Calendar.current.component(.month, from: Date())
private let currentYear = Calendar.current.component(.year, from: Date())
var body: some View {
VStack {
// Jahresnavigation mit Heute Button
HStack {
Button(action: {
year -= 1
}) {
Image(systemName: "chevron.left")
.font(.title2)
.padding()
}
// Vermeide Tausendertrennung in der Jahreszahl
Text("\(String(year))")
.font(.largeTitle)
.bold()
Button(action: {
year += 1
}) {
Image(systemName: "chevron.right")
.font(.title2)
.padding()
}
Spacer()
// Heute-Button für das Springen zum aktuellen Tag
TodayButton {
withAnimation {
scrollProxy?.scrollTo("day-\(currentDay)-\(currentMonth)", anchor: .center)
}
year = currentYear
}
}
.padding(.vertical, 10)
// Scrollbare Monatsübersicht für das gesamte Jahr
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 40) {
ForEach(1...12, id: \.self) { month in
VStack {
// Monatsüberschrift
Text("\(calendar.monthSymbols[month - 1])")
.font(.title)
.bold()
// Wochentage
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 10) {
ForEach(daysOfWeek, id: \.self) { day in
Text(day)
.font(.headline)
.frame(maxWidth: .infinity)
}
}
// Tage des Monats anzeigen
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 20) {
ForEach(getDaysInMonth(month: month, year: year), id: \.self) { day in
if day != 0 {
VStack(alignment: .leading) {
// Highlight für den aktuellen Tag durch rote Ziffer
Text("\(day)")
.font(.body) // Kleinere Ziffern
.bold()
.foregroundColor(isToday(day: day, month: month, year: year) ? .red : .black)
.padding(.bottom, 10) // Abstand zum unteren Bereich
.padding(.leading, 5)
// Platzhalter für spätere Termine, größerer Bereich
Rectangle()
.fill(Color.clear) // Hintergrundfarbe entfernen
.frame(height: 60) // Mehr Platz für Termine
}
.frame(minHeight: 120) // Vergrößerter Bereich für jeden Tag
.border(Color.gray.opacity(0.2), width: 1) // Dezenter Rahmen
.id("day-\(day)-\(month)") // Eindeutige ID für jeden Tag, damit genau dorthin gescrollt werden kann
} else {
Color.clear // Platzhalter für leere Felder
}
}
}
.padding(.horizontal)
}
}
}
.onAppear {
scrollProxy = proxy // ScrollViewProxy speichern
// Nur einmalig beim ersten Laden der Ansicht zum aktuellen Tag scrollen
if !hasScrolledToToday && year == currentYear {
DispatchQueue.main.async {
withAnimation {
proxy.scrollTo("day-\(currentDay)-\(currentMonth)", anchor: .center)
hasScrolledToToday = true // Verhindert erneutes Scrollen beim erneuten Aufrufen der View
}
}
}
}
}
}
}
}
// Funktion zur Berechnung der Tage des Monats
func getDaysInMonth(month: Int, year: Int) -> [Int] {
let dateComponents = DateComponents(year: year, month: month)
guard let firstOfMonth = calendar.date(from: dateComponents),
let range = calendar.range(of: .day, in: .month, for: firstOfMonth) else {
return []
}
let numberOfDays = range.count
let firstWeekday = calendar.component(.weekday, from: firstOfMonth) - 1 // Sonntag = 1, daher -1
var days = Array(repeating: 0, count: firstWeekday == 0 ? 6 : firstWeekday - 1) // Leerzeichen für die ersten Tage der Woche
days += Array(1...numberOfDays)
return days
}
// Überprüft, ob der aktuelle Tag angezeigt wird
func isToday(day: Int, month: Int, year: Int) -> Bool {
return day == currentDay && month == currentMonth && year == currentYear
}
}
struct TodayButton: View {
var action: () -> Void
var body: some View {
Button(action: action) {
Text("button_today")
// .font(.headline)
.padding(.horizontal)
.padding(.vertical, 8)
//.background(Color.blue.opacity(0.1))
.cornerRadius(10)
}
.padding(.trailing, 10)
}
}
No matter what I try, when I switch back from another year to the current year, the func isToday does not seem to be executed and the current date is no longer highlighted. This always seems to happen when I change the year, because as long as I only scroll up or down in the current year, the highlight remains when the page is loaded for the first time.
The problem is the use of a view identifier that consists of the day and the month. Yes, that affords the scrolling that you were looking for, but view identifiers have a second purpose, to let SwiftUI know which views need to be re-rendered. But your identifier consists of just the day and the month, so you can end up with un-rerendered days when year
is updated.
I might suggest, for example, using an identifier that captures the year, too:
private extension ContentView {
func identifier(for date: Date) -> String {
let components = calendar.dateComponents([.day, .month, .year], from: date)
return "day-\(components.day!)-\(components.month!)-\(components.year!)"
}
}
For more information about how view identifiers are used to improve performance by not re-rendering views unnecessarily, see 2021’s Demystify SwiftUI video or 2022’s Demystify SwiftUI performance.
Thus, perhaps:
import SwiftUI
struct ContentView: View {
@State private var year: Int = Calendar.current.component(.year, from: .now) // Aktuelles Jahr
@State private var scrollProxy: ScrollViewProxy? = nil
@State private var hasScrolledToToday = false // Verhindert mehrfaches Scrollen beim Laden der View
private let calendar = Calendar.current
private let daysOfWeek = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"]
var body: some View {
VStack {
// Jahresnavigation mit Heute Button
HStack {…}
.padding(.vertical, 10)
// Scrollbare Monatsübersicht für das gesamte Jahr
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 40) {
ForEach(1...12, id: \.self) { month in
VStack {
// Monatsüberschrift
Text("\(calendar.monthSymbols[month - 1])")
.font(.title)
.bold()
// Wochentage
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 10) {
ForEach(daysOfWeek, id: \.self) { day in
Text(day)
.font(.headline)
.frame(maxWidth: .infinity)
}
}
// Tage des Monats anzeigen
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 20) {
ForEach(datesInMonth(month: month, year: year), id: \.self) { date in
if let date {
let day = calendar.component(.day, from: date)
VStack(alignment: .leading) {
// Highlight für den aktuellen Tag durch rote Ziffer
Text("\(day)")
.font(.body) // Kleinere Ziffern
.bold()
.foregroundColor(calendar.isDateInToday(date) ? .red : .primary)
.padding(.bottom, 10) // Abstand zum unteren Bereich
.padding(.leading, 5)
// Platzhalter für spätere Termine, größerer Bereich
Rectangle()
.fill(Color.clear) // Hintergrundfarbe entfernen
.frame(height: 60) // Mehr Platz für Termine
}
.frame(minHeight: 120) // Vergrößerter Bereich für jeden Tag
.border(Color.gray.opacity(0.2), width: 1) // Dezenter Rahmen
.id(identifier(for: date))
} else {
Color.clear // Platzhalter für leere Felder
}
}
}
.padding(.horizontal)
}
}
}
.onAppear {
scrollProxy = proxy // ScrollViewProxy speichern
// Nur einmalig beim ersten Laden der Ansicht zum aktuellen Tag scrollen
if !hasScrolledToToday && year == currentYear {
DispatchQueue.main.async {
withAnimation {
proxy.scrollTo(identifier(for: .now), anchor: .center)
hasScrolledToToday = true // Verhindert erneutes Scrollen beim erneuten Aufrufen der View
}
}
}
}
}
}
}
}
}
private extension ContentView {
/// Funktion zur Berechnung der Tage des Monats
func datesInMonth(month: Int, year: Int) -> [Date?] {
let dateComponents = DateComponents(year: year, month: month)
guard
let firstOfMonth = calendar.date(from: dateComponents),
let range = calendar.range(of: .day, in: .month, for: firstOfMonth)
else {
return []
}
let numberOfDays = range.count
let firstWeekday = calendar.component(.weekday, from: firstOfMonth) - 1 // Sonntag = 1, daher -1
let placeholders: [Date?] = Array(repeating: nil, count: firstWeekday == 0 ? 6 : firstWeekday - 1)
let dates = (0..<numberOfDays).map { calendar.date(byAdding: .day, value: $0, to: firstOfMonth) }
return placeholders + dates
}
func identifier(for date: Date) -> String {
let components = calendar.dateComponents([.day, .month, .year], from: date)
return "day-\(components.day!)-\(components.month!)-\(components.year!)"
}
var currentYear: Int { calendar.component(.year, from: .now) }
}
struct TodayButton: View {
var action: () -> Void
var body: some View {
Button(action: action) {
Text("button_today")
// .font(.headline)
.padding(.horizontal)
.padding(.vertical, 8)
//.background(Color.blue.opacity(0.1))
.cornerRadius(10)
}
.padding(.trailing, 10)
}
}
That having been said, I might recommend a few other things:
shortStandaloneWeekdaySymbols
and factor in the firstDayOfWeek
;LazyVGrid
to make sure days of the week and the dates line up properly and don’t have different padding;.primary
instead of .black
, so it renders nicely in dark mode, andDayView
out of the main view.Thus, perhaps:
struct ContentView: View {
@State private var year: Int = Calendar.current.component(.year, from: .now) // Aktuelles Jahr
@State private var scrollProxy: ScrollViewProxy? = nil
@State private var hasScrolledToToday = false // Verhindert mehrfaches Scrollen beim Laden der View
private let calendar: Calendar = .current
var body: some View {
VStack {
// Jahresnavigation mit Heute Button
HStack {
Button (action: { year -= 1 }) {
Image(systemName: "chevron.left")
.font(.title2)
.padding()
}
// Vermeide Tausendertrennung in der Jahreszahl
Text("\(String(year))")
.font(.largeTitle)
.bold()
Button(action: { year += 1 }) {
Image(systemName: "chevron.right")
.font(.title2)
.padding()
}
Spacer()
// Heute-Button für das Springen zum aktuellen Tag
TodayButton {
year = calendar.component(.year, from: .now)
withAnimation {
scrollProxy?.scrollTo(startOfToday, anchor: .center)
}
}
}
.padding(.vertical, 10)
// Scrollbare Monatsübersicht für das gesamte Jahr
ScrollViewReader { proxy in
ScrollView {
VStack(spacing: 40) {
ForEach(1...12, id: \.self) { month in
VStack {
// Monatsüberschrift
Text("\(calendar.monthSymbols[month - 1])")
.font(.title)
.bold()
// Wochentage
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 10) {
ForEach(weekdaySymbols, id: \.self) { day in
Text(day)
.font(.headline)
.frame(maxWidth: .infinity)
}
ForEach(datesInMonth(month: month, year: year), id: \.self) { date in
if let date {
DayView(date: date)
} else {
Color.clear // Platzhalter für leere Felder
}
}
}
.padding(.horizontal)
}
}
}
.onAppear {
scrollProxy = proxy // ScrollViewProxy speichern
// Nur einmalig beim ersten Laden der Ansicht zum aktuellen Tag scrollen
if !hasScrolledToToday, year == currentYear {
DispatchQueue.main.async {
withAnimation {
hasScrolledToToday = true
proxy.scrollTo(startOfToday, anchor: .center)
}
}
}
}
}
}
}
}
}
private extension ContentView {
/// Weekday symbols, reflecting the `firstWeekday` for the device
var weekdaySymbols: [String] {
let symbols = calendar.shortStandaloneWeekdaySymbols
let startOfWeek = calendar.firstWeekday - 1
return Array(symbols[startOfWeek...]) + Array(symbols[..<startOfWeek])
}
/// Start of today’s date
var startOfToday: Date { calendar.startOfDay(for: .now) }
/// Start of today’s date
var currentYear: Int { calendar.component(.year, from: .now) }
/// Funktion zur Berechnung der Tage des Monats
func datesInMonth(month: Int, year: Int) -> [Date?] {
let dateComponents = DateComponents(year: year, month: month)
guard
let firstOfMonth = calendar.date(from: dateComponents),
let range = calendar.range(of: .day, in: .month, for: firstOfMonth)
else {
return []
}
let numberOfDays = range.count
let weekdayOfFirstOfMonth = calendar.component(.weekday, from: firstOfMonth)
let blankDays = (weekdayOfFirstOfMonth + 7 - calendar.firstWeekday) % 7
let placeholders: [Date?] = Array(repeating: nil, count: blankDays)
let dates = (0..<numberOfDays).map { calendar.date(byAdding: .day, value: $0, to: firstOfMonth) }
return placeholders + dates
}
}
struct DayView: View {
let date: Date
var body: some View {
VStack(alignment: .leading) {
Text("\(Calendar.current.component(.day, from: date))")
.font(.body) // Kleinere Ziffern
.bold()
.foregroundColor(calendar.isDateInToday(date) ? .red : .primary)
.padding(.bottom, 10) // Abstand zum unteren Bereich
.padding(.leading, 5)
Rectangle()
.fill(Color.clear) // Hintergrundfarbe entfernen
.frame(height: 60) // Mehr Platz für Termine
}
.frame(minHeight: 120) // Vergrößerter Bereich für jeden Tag
.border(Color.gray.opacity(0.2), width: 1) // Dezenter Rahmen
.id(date)
}
}
struct TodayButton: View {
var action: () -> Void
var body: some View {
Button(action: action) {
Text("button_today")
.padding(.horizontal)
.padding(.vertical, 8)
.cornerRadius(10)
}
.padding(.trailing, 10)
}
}