iOS 17下SwiftUI多日期选择器触发范围越界崩溃问题
iOS 17 多日期选择器范围越界崩溃修复方案
问题背景
视图包含多个日期选择器,主日期选择器的值决定其他日期选择器的有效范围。原本在主日期的onChange事件中调用函数确保子日期始终处于有效范围内,iOS 17(或17.2)之前功能正常,但升级后出现崩溃:主日期变更时,子日期先处于范围外,触发以下错误:
"start date cannot be less than end date"
"Invalid state. Unable to find a lower bounds in range"
要求保持所有日期选择器可见,同时确保子日期在主日期变更时自动调整到有效范围。
问题根源
iOS 17中SwiftUI的视图更新优先级和顺序发生变化,onChange的执行时机晚于视图根据新状态重新计算范围的时刻,导致子日期选择器先使用新范围检查旧的子日期值,触发越界错误。
解决方案
1. 将子日期调整逻辑移到主日期的didSet中
主日期变更时立刻同步调整子日期,避免视图读取到无效的状态组合。
2. 修复子日期选择器的绑定问题
替换原计算属性绑定为自定义绑定,解决可选值无法直接修改的问题。
3. 确保日期范围的安全性
增加边界检查,避免生成min > max的无效ClosedRange。
修正后的完整代码
修改后的ItemAdd类
@MainActor class ItemAdd: ObservableObject { @Published var mainDate: Date = Date() { // 主日期变更时立刻调整子日期到有效范围 didSet { adjustSecondDateToValidRange() } } @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 secondDateBinding: Binding<Date> { Binding( get: { self.secondDate ?? self.mainDate }, set: { self.secondDate = $0 } ) } private func adjustSecondDateToValidRange() { let validRange = dateRangeSecondDate() if let currentSecondDate = secondDate { // 将日期限制在有效范围内 secondDate = validRange.clamped(to: currentSecondDate) } else { secondDate = validRange.upperBound } } private func dateRangeSecondDate() -> ClosedRange<Date> { let min = mainDate guard let nextYear = Calendar.current.date(byAdding: .year, value: 1, to: min) else { return min...min } guard let max = Calendar.current.date(byAdding: .day, value: -1, to: nextYear) else { return min...min } // 确保范围始终有效,避免min > max的情况 return min <= max ? min...max : max...min } }
修改后的ViewModel和MyView
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> { let today = Date() guard let min = Calendar.current.date(byAdding: .day, value: -100, to: today), let max = Calendar.current.date(byAdding: .day, value: 100, to: today) else { return today...today } return min...max } } } struct MyView: View { @ObservedObject var itemAdd: ItemAdd @StateObject var viewModel: ViewModel var body: some View { VStack { Text("Main Date: \(itemAdd.dateToString(date: itemAdd.mainDate))") DatePicker( "Main Date", selection: $itemAdd.mainDate, in: viewModel.dateRangeMainDate(), displayedComponents: [.date] ) .datePickerStyle(GraphicalDatePickerStyle()) .labelsHidden() .frame(width: 320) Text("Second Date: \(itemAdd.dateToString(date: itemAdd.secondDate ?? itemAdd.mainDate))") DatePicker( "Second Date", selection: itemAdd.secondDateBinding, in: itemAdd.dateRangeSecondDate(), displayedComponents: [.date] ) .datePickerStyle(GraphicalDatePickerStyle()) .labelsHidden() .frame(width: 320) } } }
关键修改说明
- 主日期
didSet同步调整:确保主日期变更后立刻修正子日期,让视图始终读取到有效状态,规避iOS17视图更新顺序问题。 - 自定义绑定:解决可选值无法直接绑定到日期选择器的问题,保证读写操作正确作用于
secondDate。 - 范围有效性检查:在计算子日期范围时增加边界判断,避免生成无效范围导致崩溃。
- 移除加载状态切换:状态调整同步完成,无需隐藏视图即可保持所有组件可见。
内容的提问来源于stack exchange,提问作者kittonian
相关产品推荐
相关产品推荐

