我有一个包含多个日期选择器的视图。主日期选择器的值确定其他日期选择器的正确范围,并且正在运行主日期选择器的 onChange 函数以确保这些值始终在范围内。
这工作得很好,但自从 iOS 17(或者可能是 17.2 不确定)以来,应用程序因超出范围错误而崩溃,因为视图没有按正确的顺序更新。视图看到 mainDate 的更改,但由于 SecondDate 超出范围而崩溃,而不是运行 setSecondDate() 并正确更新视图。
我不想为了解决问题而隐藏任何观点。用户必须能够同时看到所有内容。
当 mainDate 更改时,我需要 secondaryDate 正确更新,以便它始终保持在范围内。
通过日期选择器设置 mainDate 变量时会发生这种情况,尤其是在更改年份时。如果时间向前推进,我会得到一个错误
"start date cannot be less than end date"
但如果时光倒流,我就会明白
"Invalid state. Unable to find a lower bounds in range"
这是我的代码(尽可能精简): 假设 ItemAdd 已经实例化,并且 secondaryDate 设置为 mainDate。
@MainActor
class ItemAdd: ObservableObject {
@Published var mainDate: Date = Date()
@Published var secondDate: Date?
@Published var thirdDate: Date?
func dateToString(date: Date) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ssZZZZZ"
dateFormatter.dateStyle = .long
return dateFormatter.string(from: date)
}
var secondDateDate: Date {
secondDate ?? Date()
}
var thirdDateDate: Date {
thirdDate ?? Date()
}
}
extension MyView {
@MainActor
class ViewModel: ObservableObject {
weak var itemAdd: ItemAdd?
@Published var loadState: LoadState = .loaded
init(itemAdd: ItemAdd?) {
self.itemAdd = itemAdd
}
enum LoadState {
case loaded
case loading
}
func dateRangeMainDate() -> ClosedRange<Date> {
// General Variables
var min: Date = Date()
var max: Date = Date()
guard let itemAdd = itemAdd else { return min...max }
min = Calendar.current.date(
byAdding: .day,
value: -100,
to: Date()
) ?? Date()
max = Calendar.current.date(
byAdding: .day,
value: 100,
to: Date()
) ?? Date()
// Return the result
return min...max
}
func dateRangeSecondDate() -> ClosedRange<Date> {
// General Variables
var min: Date = Date()
var max: Date = Date()
guard let itemAdd = itemAdd else { return min...max }
min = mainDate
var dateCalc = Calendar.current.date(
byAdding: .year,
value: 1,
to: mainDate
) ?? Date()
dateCalc = Calendar.current.date(
byAdding: .day,
value: -1,
to: dateCalc
) ?? Date()
max = dateCalc
// Return the result
return min...max
}
func setSecondDate() {
guard let itemAdd = itemAdd else { return }
if let newSecondDate = itemAdd.secondDate {
if !self.dateRangeSecondDate().contains(newSecondDate) {
itemAdd.secondDate = self.dateRangeSecondDate().upperBound
}
} else {
itemAdd.secondDate = self.dateRangeSecondDate().upperBound
}
}
}
}
struct MyView: View {
@ObservedObject var itemAdd: ItemAdd
@StateObject var viewModel: ViewModel
var body: some View {
VStack {
Text("Main Date: \(itemAdd.dateToFullString(date: itemAdd.mainDate))")
DatePicker(
"Main Date",
selection: itemAdd.mainDate,
in: viewModel.dateRangeMainDate(),
displayedComponents: [.date]
)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
.frame(width: 320)
.onChange(of: itemAdd.mainDate) {
viewModel.loadState = .loading
viewModel.setSecondDate()
viewModel.loadState = .loaded
}
switch viewModel.loadState {
case .loaded:
Text("Second Date: \(itemAdd.dateToFullString(date: itemAdd.secondDateDate))")
DatePicker(
"Second Date",
selection: itemAdd.secondDateDate,
in: viewModel.dateRangeSecondDate(),
displayedComponents: [.date]
)
.datePickerStyle(GraphicalDatePickerStyle())
.labelsHidden()
.frame(width: 320)
case .loading:
ProgressView()
}
}
}
}
您的问题是因为您使用相同的通用变量
min
和 max
在运行时评估选择器边界。而且,您正在更新评估边界的函数内的边界。
每次调用
.onChange
闭包并更新 mainDate 时,都会重新绘制 UI,并且 dateRangeSecondDate
可能会突破一年以上的界限!这非常令人困惑,您实际上无法预测首先调用哪个范围更新函数。
我的方法是根据需要在主选择器的
min
关闭中更新 max
和 .onChange
only(我可以看到这实际上并不需要,因为它是前后 100 天的固定范围)今天)。 然后,函数
dateRangeMainDate
和 dateRangeSecondDate
将在运行时评估正确的边界并返回它。
请参阅此处的简化示例:
var min: Date = Calendar.current.date(byAdding: .day, value: -100, to: Date()) ?? Date()
var max: Date = Calendar.current.date(byAdding: .day, value: 100, to: Date()) ?? Date()
// potential mainDate binding variable
var mainDate: Date = Date()
func dateRangeMainDate() -> ClosedRange<Date> {
// do any evaluation here if needed, with local variables and return, but don't update directly the min and max here
return min...max
}
func dateRangeSecondDate() -> ClosedRange<Date> {
var finalDate = Calendar.current.date(
byAdding: .year,
value: 1,
to: mainDate
) ?? Date()
finalDate = Calendar.current.date(
byAdding: .day,
value: -1,
to: finalDate
) ?? Date()
return mainDate...finalDate
}
请记住,函数给出范围的目的并不是增加更多的复杂性。只需评估边界并返回它。我希望这有帮助。