如何有效地更新SwiftUI DatePicker,使其不允许未来时间,但始终允许当前时间?

xdyibdwo  于 2023-04-04  发布在  Swift
关注(0)|答案(1)|浏览(104)

我有一个SwiftUI View,它包含一个DatePicker.datePickerStyle(.wheel)。我希望用户能够选择过去到当前时间的日期和时间,但不能选择未来的日期或时间。
有一个非常简单的方法可以实现大部分目标:

struct ContentView: View {
    
    @State var selectedDate = Date()
    
    var body: some View {
        VStack {
            DatePicker("Select Date and Time",
                       selection: $selectedDate,
                       in: ...Date(),
                       displayedComponents: [.date, .hourAndMinute])
                .labelsHidden()
                .datePickerStyle(.wheel)
        }
        .padding()
    }
}

但是,在下一分钟开始后,最大值将过期:用户只能根据创建View的日期和时间选择时间。
例如,如果包含DatePickerView在12:44呈现,则一旦时间是12:45,用户仍然只能拾取直到并包括12:44的时间,而不能拾取12:45。
为了解决这个问题,我创建了一个ViewModel,其中@PublishedmaxDate每分钟更新一次,并将其用作DatePicker

class ContentViewModel: ObservableObject {
    @Published var maxDate = Date()
    private var timer: Timer?

    init() {
        updateMaxDate()
    }

    func updateMaxDate() {
        timer?.invalidate()

        let now = Date()
        let calendar = Calendar.current
        let currentMinuteStart = calendar
            .date(bySettingHour: now.hour,
                  minute: now.minute,
                  second: 0,
                  of: now)!
        let nextMinuteStart = calendar
            .date(byAdding: .minute,
                  value: 1,
                  to: currentMinuteStart)!

        let remainingSeconds = calendar
            .dateComponents([.second],
                            from: now,
                            to: nextMinuteStart)

        if let seconds = remainingSeconds.second {
            maxDate = currentMinuteStart
            timer = Timer
                .scheduledTimer(withTimeInterval: TimeInterval(seconds),
                                         repeats: false)
            { [weak self] _ in
                self?.timerFired()
            }
        }
    }

    private func timerFired() {
        print("timerFired()")
        updateMaxDate()
    }

    deinit {
        timer?.invalidate()
    }
}

struct ContentView: View {
    @StateObject private var viewModel = ContentViewModel()
    
    @State var selectedDate = Date()
    
    var body: some View {
        VStack {
            DatePicker("Select Date and Time",
                       selection: $selectedDate,
                       in: ...viewModel.maxDate,
                       displayedComponents: [.date, .hourAndMinute])
                .labelsHidden()
                .datePickerStyle(.wheel)
        }
        .padding()
    }
}

extension Date {
    var hour: Int { Calendar.current.component(.hour, from: self) }
    var minute: Int { Calendar.current.component(.minute, from: self) }
}

这是可行的,但有一个问题,我不能弄清楚:**我希望timerFired函数在每一分钟都只被调用一次,但实际上它被调用了数百次。
timerFired被调用的确切次数各不相同,但似乎通常在700-1000次左右,并导致明显的CPU峰值。
我以为这可能与视图刷新或ContentViewModel对象的多个示例有关,但它是一个@StateObject,所以它只应该被创建一次,并且我能够通过向其init添加print语句来确认它只被初始化一次。
如何解决此问题,使maxDate只更新一次?或者,是否有更好的方法来设置DatePicker以实现相同的行为?

oaxa6hgo

oaxa6hgo1#

我通过更新ContentViewModel找到了一个解决方案,如下所示:

class ContentViewModel: ObservableObject {
    @Published var maxDate = Date()
    private var timer: Timer?

    init() {
        updateMaxDate()
    }

    private func updateMaxDate() {
        timer?.invalidate()
        
        // The number of seconds that have passed in the current minute
        guard let currentDateSecondsComponent: Int =
            Calendar.current
            .dateComponents([.second],
                            from: Date())
            .second
            else { return }

        // Figure out how many seconds it is until the next refresh
        let timeUntilNextRefresh: TimeInterval =
            Double(60 - currentDateSecondsComponent)

        timer = Timer
            .scheduledTimer(withTimeInterval: timeUntilNextRefresh,
                            repeats: false)
        { [weak self] _ in

            // Set the max DatePicker date to the current date
            self?.maxDate = Date()

            // Schedule the next update timer
            self?.updateMaxDate()
        }
    }

    deinit {
        timer?.invalidate()
    }
}

在进一步的测试中,当我使用DateComponent方法来确定到下一分钟的秒数时,它会在新的一分钟之前的几分之一秒内调度一个计时器,并且由于每个计时器触发都会调度另一个计时器,它会触发很多次,直到新的一分钟真正开始。
我切换到从60减去,我知道这不是DateComponent的方式,但它绕过了这个分数秒的问题,我不能预见会有任何重大问题。

更新:我找到了一个使用DateComponent的工作解决方案

经过进一步的实验,我找到了一个解决方案,将我的工作答案与DateComponent方法结合起来,同时避免了函数被调用数百次的问题:

class ContentViewModel: ObservableObject {
    @Published var maxDate = Date()
    private var timer: Timer?

    init() {
        setUpTimer()
    }

    private func setUpTimer() {
        let now = Date()
        let calendar = Calendar.current
        let nextMinuteStart = calendar
            .date(byAdding: .second,
                  value: 60 - calendar.component(.second,
                                                 from: now),
                  to: now)!

        let remainingSeconds = calendar
            .dateComponents([.second],
                            from: now,
                            to: nextMinuteStart)

        if let seconds = remainingSeconds.second {
            timer?.invalidate()
            timer = Timer
                .scheduledTimer(withTimeInterval: TimeInterval(seconds),
                                repeats: false)
            { [weak self] _ in
                self?.maxDate = Date()
                self?.setUpTimer()
            }
        }
    }

    deinit {
        timer?.invalidate()
    }
}

相关问题