视图中的多个日期选择器导致超出范围错误

问题描述 投票:0回答:1

我有一个包含多个日期选择器的视图。主日期选择器的值确定其他日期选择器的正确范围,并且正在运行主日期选择器的 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()
            }
        }
    }
}
swiftui view datepicker outofrangeexception
1个回答
0
投票

您的问题是因为您使用相同的通用变量

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
}

请记住,函数给出范围的目的并不是增加更多的复杂性。只需评估边界并返回它。我希望这有帮助。

© www.soinside.com 2019 - 2024. All rights reserved.