Search code examples
swiftdateswiftuiswiftdata

SwiftUI sheet for foreach not opening after index 4


I have a simple view that displays all days of a month like the apple calendar app does. The view is rendered for each month. In the view I show circles (BubbleView) that function as buttons. When I click a bubble, a sheet is opened. Either the creation, or the edit view.

From January (int 1) to March (int 3) it works perfectly fine. But from April (int 4) onwards, the bubbles I tap won't open my sheet. Can anybody give me a hint why my sheet might not be opening?

I'm new to SwiftUI, so please excuse my probably not very pretty code. I'm just getting a hang of it.

I've been trying all day. First I thought it might be a problem with daylight saving time. So I changed my calendar. Each day now has a timestamp from 00:00:00. So that should not be the problem. I'm at a point where I don't even know what I should google.

I've also checked if my bool value to open the sheet is true. It is. I also reach the else part of my function each time. Just the sheet does not open.

Since I am new to SwiftUI, I don't really know how to debug something like this. I've tried print statements and breakpoints. It all behaves exactly identical. I... I don't know.

Help would be very much appreciated.

Here is the code for my month view:

import SwiftUI
import SwiftData

struct CalendarMonthView: View {
    @Query(sort: \CalendarDay.date) var days: [CalendarDay]
    @State private var selectedDate: Date = getStartAndEndDatesForWeek()[0].date
    @State private var selectedSavedDate: CalendarDay?
    @State var isSheetPresenting = false
    
    var month: Int
    var year: Int
    
    init(month: Int, year: Int) {
        self.month = month
        self.year = year
        _days = Query(
            filter: #Predicate<CalendarDay> { day in
                day.monthNumber == month && day.yearNumber == year
            },
            sort: \CalendarDay.date
        )
    }
    
    var body: some View {
        VStack {
            LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 7), spacing: 20) {
                ForEach(fetchDatesForMonth(month: month)) { value in
                    let savedDay = days.first(where: { $0.date == value.date })
                    ZStack {
                        if value.date < getMaxDate() {
                            Button(action: {
                                print(value.date)
                                handleBubbleTap(savedDay, value.date)
                            }){
                                DayBubbleView(date: value.date, color: getBubbleColor(day: savedDay), opacity: getBubbleOpacity(day: savedDay))
                            }
                            .buttonStyle(PlainButtonStyle())
                        } else {
                            Text("")
                        }
                    }
                    .frame(width: 32, height: 32)
                }
            }
        }
        .sheet(isPresented: $isSheetPresenting) {
            DayCreateView(date: $selectedDate)
        }
        .sheet(item: $selectedSavedDate) { date in
            DayEditView(day: date)
        }
    }
    
    func handleBubbleTap(_ savedDay: CalendarDay?, _ date: Date) {
        selectedDate = date
        print(selectedDate)
        if (savedDay != nil) {
            selectedSavedDate = savedDay
        } else {
            isSheetPresenting = true
            print(isSheetPresenting)
        }
    }
    
    func fetchDatesForMonth(month: Int) -> [WeekDate] {
        let genericTimeZone = TimeZone.init(secondsFromGMT: 0)!
        var genericCalendar = Calendar(identifier: .gregorian)
        genericCalendar.timeZone = genericTimeZone
        genericCalendar.locale = NSLocale(localeIdentifier: "de_DE") as Locale
        genericCalendar.firstWeekday = 2
        
        var dates = generateDatesForMonth(month: month) ?? []
        
        let firstDayOfWeek = (genericCalendar.component(.weekday, from: dates.first?.date ?? Date()) - genericCalendar.firstWeekday + 7) % 7
        
        for _ in 0..<firstDayOfWeek {
            dates.insert(WeekDate(date: getMaxDate()), at: 0)
        }
        
        return dates
    }
    
    func generateDatesForMonth(month: Int) -> [WeekDate]? {
        let genericTimeZone = TimeZone.init(secondsFromGMT: 0)!
        var genericCalendar = Calendar(identifier: .gregorian)
        genericCalendar.timeZone = genericTimeZone
        genericCalendar.locale = NSLocale(localeIdentifier: "de_DE") as Locale
        genericCalendar.firstWeekday = 2
        
        let currentYear = genericCalendar.component(.year, from: Date())
        
        // Create a date component with the specified month and year
        var dateComponents = DateComponents()
        dateComponents.year = currentYear
        dateComponents.month = month
        
        // Get the first day of the specified month
        guard let firstDayOfMonth = genericCalendar.date(from: dateComponents) else {
            return nil
        }
        
        // Get the range of days in the specified month
        guard let range = genericCalendar.range(of: .day, in: .month, for: firstDayOfMonth) else {
            return nil
        }
        
        // Generate an array of dates for the entire month
        let dates = (range.lowerBound..<range.upperBound).compactMap { day in
            var components = DateComponents()
            components.year = currentYear
            components.month = month
            components.day = day
            return WeekDate(date: genericCalendar.date(from: components)!)
        }
        
        return dates
    }
    
    func getMaxDate() -> Date {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy/MM/dd HH:mm"
        return formatter.date(from: "9999/01/01 04:20")!
    }
}

#Preview {
    CalendarMonthView(month: 5, year: 2025)
}

And here is the code of the parent, that renders the month view 12 times. The idea is a similar layout as the activity app from apple.

import SwiftUI
import SwiftData

struct YearView: View {
    
    var body: some View {
        ScrollViewReader { value in
            ScrollView {
                LazyVStack(alignment: .leading,spacing: 0, pinnedViews: [.sectionHeaders]) {
                    ForEach(0..<12) { val in
                        Section(header: SectionHeaderView(month: val + 1)) {
                            CalendarMonthView(month: val + 1, year: getYearNumber(date: Date.now))
                                .padding()
                        }
                    }
                }
            }
        }
        .navigationBarTitle(Text(""), displayMode: .inline)
    }
}

#Preview {
    YearView()
}

Edit: Looks like it has something to do with the LazyVStack. I just changed my list to just render April through December and it turns out my sheet just simply won't open any item past the third one in my list.


Solution

  • The problem was that my MonthView was rendered inside a LazyVStack and all months not initially visible weren't showing the sheets for bubble taps.

    With the comments pointing me in the right direction, I refactored my component to take my sheets out of the MonthView component and put them into the YearView component, since this is the view I want to lay my sheets over.