Search code examples
swiftforeachswiftuipicker

SwiftUI: how to update the range of a ForEach loop based on the value of a Picker


So I'm trying to have a ForEach loop update the amount of times a View is looped based on what Month (value in a picker) is selected. In my case, they will be looping based on the number of days in the month of the selected month for the given year. I already have a function that gives me the number of days in each month, however, when I plug that into the ForEach Loop, it only gets run based on the first month selected and stays iterating the number of days of that month for the rest. Here is my code for the ForEach Loop:

ForEach(0..<getRange(year: yearIndex, month: monthIndex + indexCheck)) { i in
     NavigationLink(destination: ContentView(day: yearData[yearIndex].months[monthIndex].dayInfo[i])) {
          DayRow(day: yearData[yearIndex].months[monthIndex].dayInfo[i])
     }
}

and here is the getRange() Function:

func getRange(year: Int, month: Int) -> Int {
        return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year + 2020, month: month + 1))!)!.count
}

The yearIndex variable is linked to the picker value of three years, (2020, 2021, 2022). Here is the code for it:

Picker("Years", selection: $yearIndex) {
     ForEach(0 ..< year.count) { i in
          Text(String(self.year[i])).tag(i)
     }
}
.pickerStyle(SegmentedPickerStyle())

The monthIndex variable is linked to the picker with the months in the year (Jan-Dec). Here is the code for it:

Picker("Month", selection: $monthIndex) {
     ForEach(0 ..< monthArray.count) { i in
          Text(self.monthArray[i]).tag(i)
     }
}
.padding(.bottom, 2)

I'm not sure what I'm doing wrong, and I'm not sure how to do this, so any help would be greatly appreciated! I'm still quite new to Swift/SwiftUI, so any advice to better code this would also be appreciated!

EDIT: Here is a minimal reproducible example as requested:

struct ContentView: View {
    
    @State var year = [2020, 2021, 2022]
    //monthSymbols gets an array of all the months
    @State var monthArray = DateFormatter().monthSymbols!
    @State var yearIndex = 0
    @State var monthIndex = 0
    @State var indexCheck = 0
    @State var indexTest = 0
    
    var body: some View {
        NavigationView {
            List {
                Section {
                    VStack {
                        Picker("Years", selection: $yearIndex) {
                            ForEach(0 ..< year.count) { i in
                                Text(String(self.year[i])).tag(i)
                            }
                        }
                        .pickerStyle(SegmentedPickerStyle())
                        
                        Divider()
                        
                            Picker("Month", selection: $monthIndex) {
                                ForEach(0 ..< monthArray.count) { i in
                                    Text(self.monthArray[i]).tag(i)
                                }
                            }
                            .padding(.bottom, 2)
                        }
                    }
                    Section(header: Text("What I love about you")) {
                        ForEach(0..<getRange(year: yearIndex, month: monthIndex + indexCheck)) { i in
                            NavigationLink(destination: DetailsView()) {
                                Text("Row \(i)")
                            }
                        }
                    }
            }
            .listStyle(InsetGroupedListStyle())
            .navigationBarTitle(Text("\(monthArray[monthIndex + indexCheck]) \(String(year[yearIndex]))"))
        }
        
    }
    func getRange(year: Int, month: Int) -> Int {
        return Calendar.current.range(of: .day, in: .month, for: Calendar.current.date(from: DateComponents(year: year + 2020, month: month + 1))!)!.count
    }
}

struct YearView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • I cleaned a little bit your code so it's more readable.

    Here is the ContentView:

    struct ContentView: View {
        let yearArray = [2020, 2021, 2022]
        let monthArray = DateFormatter().monthSymbols!
    
        // you don't need to operate on indices, you can use real values
        @State var selectedYear = 2020
        @State var selectedMonth = 1
    
        // improved readability
        var combinedYearMonth: String {
            "\(monthArray[selectedMonth - 1]) \(selectedYear)"
        }
    
        var body: some View {
            NavigationView {
                List {
                    pickerSection
                    daySelectionSection
                }
                .listStyle(InsetGroupedListStyle())
                .navigationBarTitle(combinedYearMonth)
            }
        }
    }
    

    The part responsible for displaying list sections:

    // sections extracted to a private extension (you can still have everything in one `ContentView` struct if you want)
    private extension ContentView {
        var pickerSection: some View {
            Section {
                yearPicker
                monthPicker
            }
        }
    
        var daySelectionSection: some View {
            Section(header: Text("What I love about you")) {
                ForEach(dayRange, id: \.self) { day in
                    NavigationLink(destination: DetailsView()) {
                        Text("Day \(day)")
                    }
                }
            }
        }
    
        // create a range of days in the `selectedMonth` for the `selectedYear`
        var dayRange: Range<Int> {
            let dateComponents = DateComponents(year: selectedYear, month: selectedMonth)
            let calendar = Calendar.current
            let date = calendar.date(from: dateComponents)!
            return calendar.range(of: .day, in: .month, for: date)!
        }
    }
    

    And the part with pickers:

    private extension ContentView {
        var yearPicker: some View {
            Picker("Years", selection: $selectedYear) {
                ForEach(yearArray, id: \.self) { year in
                    Text(String(year)) // <- no need for `.tag()` if the `id` parameter in `ForEach` is specified
                }
            }
            .pickerStyle(SegmentedPickerStyle())
        }
    
        var monthPicker: some View {
            Picker("Month", selection: $selectedMonth) {
                ForEach(1...12, id: \.self) { month in
                    Text(self.monthArray[month - 1])
                }
            }
            .padding(.bottom, 2)
        }
    }