SwiftUI:基于DateInterval的UIRepresentable日期选择器仅显示区间内月份
解决方案
问题核心:你修改了data数组的月份数据,但UIPickerView不会自动感知数据源变化,必须手动调用刷新方法才能更新视图。
1. 修改PickerView,添加数据刷新逻辑
在updateUIView方法中,先调用reloadAllComponents()触发UIPickerView刷新数据,再设置选中行:
struct PickerView: UIViewRepresentable { var data: [[String]] @Binding var selections: [Int] func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIView(context: Context) -> UIPickerView { let picker = UIPickerView(frame: .zero) picker.dataSource = context.coordinator picker.delegate = context.coordinator return picker } func updateUIView(_ view: UIPickerView, context: Context) { // 先刷新所有组件,确保新数据生效 view.reloadAllComponents() // 设置选中行 for i in 0..<self.selections.count { view.selectRow(self.selections[i], inComponent: i, animated: true) } context.coordinator.parent = self } class Coordinator: NSObject, UIPickerViewDataSource, UIPickerViewDelegate { var parent: PickerView init(_ pickerView: PickerView) { self.parent = pickerView } func numberOfComponents(in pickerView: UIPickerView) -> Int { parent.data.count } func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { parent.data[component].count } func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { parent.data[component][row] } func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { parent.selections[component] = row } } }
2. 完善DatePicker_Interval的月份切换逻辑
- 添加中间年份的默认处理(显示全部12个月)
- 重置月份选中索引,避免因数据源长度变化导致索引越界
- 统一使用全局DateFormatter,避免重复创建影响性能
struct DatePicker_Interval: View{ @State var data: [[String]] @State private var selections: [Int] = [0,0] @Environment(\.calendar) var calendar let interval: DateInterval // 全局初始化月份格式化器 private let monthFormatter: DateFormatter = { let formatter = DateFormatter() return formatter }() init(interval: DateInterval) { let startYear = calendar.component(.year, from: interval.start) let endYear = calendar.component(.year, from: interval.end) self._data = State(initialValue: [ Array(startYear...endYear).map { "\($0)" }, Array(1...12).map { monthFormatter.monthSymbols[$0-1] } ]) self.interval = interval } var body: some View{ VStack{ PickerView(data: data, selections: $selections) .onChange(of: selections[0], perform: { _ in let selectedYearStr = data[0][selections[0]] let startYearStr = String(calendar.component(.year, from: interval.start)) let endYearStr = String(calendar.component(.year, from: interval.end)) if selectedYearStr == startYearStr { // 选中起始年份,显示从起始月份到12月 let startMonth = calendar.component(.month, from: interval.start) data[1] = Array(startMonth...12).map { monthFormatter.monthSymbols[$0-1] } selections[1] = 0 // 重置选中第一个有效月份 } else if selectedYearStr == endYearStr { // 选中结束年份,显示从1月到结束月份 let endMonth = calendar.component(.month, from: interval.end) data[1] = Array(1...endMonth).map { monthFormatter.monthSymbols[$0-1] } selections[1] = 0 // 重置选中第一个有效月份 } else { // 选中中间年份,显示全部12个月 data[1] = Array(1...12).map { monthFormatter.monthSymbols[$0-1] } selections[1] = 0 // 重置选中第一个月份 } }) HStack{ Text("年份索引: \(selections[0])") Text("月份索引: \(selections[1])") } Text("区间起始: \(interval.start.description)") Text("区间结束: \(interval.end.description)") } } }
3. 保留原ContentView代码不变
struct ContentView: View { @Environment(\.calendar) var calendar private var testInterval: DateInterval { let comp = DateComponents(year: 2001, month: 5) let comp2 = DateComponents(year: 2002, month: 8) return DateInterval(start: calendar.date(from: comp) ?? Date(), end: calendar.date(from: comp2) ?? Date()) } var body: some View { DatePicker_Interval(interval: testInterval) } }
关键说明
- UIPickerView依赖数据源方法返回数据,当数据源变化时必须调用
reloadAllComponents()或reloadComponent(_:)通知视图刷新 - 切换年份时重置月份选中索引,防止原索引超出新数据源的长度范围导致崩溃
- 复用DateFormatter避免频繁创建对象,提升性能
内容的提问来源于stack exchange,提问作者sheldor
相关产品推荐
相关产品推荐

