簡體   English   中英

SwiftUI 計時器觸發會干擾工作表中的選擇器並重置其選擇

[英]SwiftUI Timer firings interferes with Picker within a sheet and resets its selection

我正在嘗試構建一個多計時器應用程序,這是目前代碼的重要部分:

struct TimerModel: Identifiable {
    var id: String = UUID().uuidString
    var title: String
    var startTime: Date? {
        didSet {
            alarmTime = Date(timeInterval: duration, since: startTime ?? Date())
        }
    }
    var pauseTime: Date? = nil
    var alarmTime: Date? = nil
    var duration: Double
    var timeElapsed: Double = 0 {
        didSet {
            displayedTime = (duration - timeElapsed).asHoursMinutesSeconds
        }
    }
    var timeElapsedOnPause: Double = 0
    var remainingPercentage: Double = 1
    var isRunning: Bool = false
    var isPaused: Bool = false
    var displayedTime: String = ""
    
    init(title: String, duration: Double) {
        self.duration = duration
        self.title = title
        self.displayedTime = self.duration.asHoursMinutesSeconds
    }
}
class TimerManager: ObservableObject {
    
    @Published var timers: [TimerModel] = [] // will hold all the timers

    @Published private var clock: AnyCancellable?
    
    private func startClock() {
        clock?.cancel()
        
        clock = Timer
            .publish(every: 1, on: .main, in: .common)
            .autoconnect()
            .sink { [weak self] _ in
                guard let self = self else { return }
                
                for index in self.timers.indices {
                    self.updateTimer(forIndex: index)
                }
            }
    }
    
    private func stopClock() {
        let shouldStopClock: Bool = true
        
        for timer in timers {
            if timer.isRunning && !timer.isPaused {
                return
            }
        }
        
        if shouldStopClock {
            clock?.cancel()
        }
    }
    
    private func updateTimer(forIndex index: Int) {
        if self.timers[index].isRunning && !self.timers[index].isPaused {
            self.timers[index].timeElapsed = Date().timeIntervalSince(self.timers[index].startTime ?? Date())
            self.timers[index].remainingPercentage = 1 - self.timers[index].timeElapsed / self.timers[index].duration
            
            if self.timers[index].timeElapsed < self.timers[index].duration {
                let remainingTime = self.timers[index].duration - self.timers[index].timeElapsed
                self.timers[index].displayedTime = remainingTime.asHoursMinutesSeconds
            } else {
                self.stopTimer(self.timers[index])
            }
        }
    }
    
    func createTimer(title: String, duration: Double) {
        let timer = TimerModel(title: title, duration: duration)
        timers.append(timer)
        startTimer(timer)
    }
    
    func startTimer(_ timer: TimerModel) {
        startClock()
        
        if let index = timers.firstIndex(where: { $0.id == timer.id }) {
            timers[index].startTime = Date()
            timers[index].isRunning = true
        }
    }
    
   func pauseTimer(_ timer: TimerModel) {
        if let index = timers.firstIndex(where: { $0.id == timer.id }) {
            timers[index].pauseTime = Date()
            timers[index].isPaused = true
        }
        
        stopClock()
    }
    
    func resumeTimer(_ timer: TimerModel) {
        startClock()
        
        if let index = timers.firstIndex(where: { $0.id == timer.id }) {
            timers[index].timeElapsedOnPause = Date().timeIntervalSince(self.timers[index].pauseTime ?? Date())
            timers[index].startTime = Date(timeInterval: timers[index].timeElapsedOnPause, since: timers[index].startTime ?? Date())
            timers[index].isPaused = false
        }
    }
    
    func stopTimer(_ timer: TimerModel) {
        if let index = timers.firstIndex(where: { $0.id == timer.id }) {
            timers[index].startTime = nil
            timers[index].alarmTime = nil
            timers[index].isRunning = false
            timers[index].isPaused = false
            timers[index].timeElapsed = 0
            timers[index].timeElapsedOnPause = 0
            timers[index].remainingPercentage = 1
            timers[index].displayedTime = timers[index].duration.asHoursMinutesSeconds
        }
        
        stopClock()
    }
    
    func deleteTimer(_ timer: TimerModel) {
        timers.removeAll(where: { $0.id == timer.id })
        
        stopClock()
    }
}
struct MainView: View {
    @EnvironmentObject private var tm: TimerManager
    
    @State private var showAddTimer: Bool = false
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(tm.timers) { timer in
                    TimerRowView(timer: timer)
                        .listRowInsets(.init(top: 4, leading: 20, bottom: 4, trailing: 4))
                }
            }
            .listStyle(.plain)
            .navigationTitle("Timers")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        showAddTimer.toggle()
                    } label: {
                        Image(systemName: "plus")
                    }
                }
            }
            .sheet(isPresented: $showAddTimer) {
                AddTimerView()
            }
        }
    }
}
struct AddTimerView: View {
    
    @EnvironmentObject private var tm: TimerManager
    
    @Environment(\.dismiss) private var dismiss
    
    @State private var secondsSelection: Int = 0
    
    private var seconds: [Int] = [Int](0..<60)

    
    var body: some View {
        NavigationStack {
            VStack {
                secondsPicker
                Spacer()
            }
            .navigationTitle("Add Timer")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .cancellationAction) {
                    Button {
                        dismiss()
                    } label: {
                        Text("Cancel")
                    }
                }
                
                ToolbarItem(placement: .confirmationAction) {
                    Button {
                        tm.createTimer(title: "Timer added from View", duration: getPickerDurationAsSeconds())
                        dismiss()
                    } label: {
                        Text("Save")
                    }
                    .disabled(getPickerDurationAsSeconds() == 0)
                }
            }
        }
    }
}
extension AddTimerView {
    private var secondsPicker: some View {
        Picker(selection: $secondsSelection) {
            ForEach(seconds, id: \.self) { index in
                Text("\(index)").tag(index)
                    .font(.title3)
            }
        } label: {
            Text("Seconds")
        }
        .pickerStyle(.wheel)
        .labelsHidden()
    }

    private func getPickerDurationAsSeconds() -> Double {
        var duration: Double = 0
        
        duration += Double(hoursSelection) * 60 * 60
        duration += Double(minutesSelection) * 60
        duration += Double(secondsSelection)
        
        return duration
    }
}
extension TimeInterval {
    
    var asHoursMinutesSeconds: String {
        if self > 3600 {
            return String(format: "%0.0f:%02.0f:%02.0f",
                   (self / 3600).truncatingRemainder(dividingBy: 3600),
                   (self / 60).truncatingRemainder(dividingBy: 60).rounded(.down),
                   truncatingRemainder(dividingBy: 60).rounded(.down))
        } else {
            return String(format: "%02.0f:%02.0f",
                   (self / 60).truncatingRemainder(dividingBy: 60).rounded(.down),
                   truncatingRemainder(dividingBy: 60).rounded(.down))
        }
        
    }
}

extension Date {
    
    var asHoursAndMinutes: String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateStyle = .none
        dateFormatter.timeStyle = .short
        
        return dateFormatter.string(from: self)
    }
}

問題是,如果我讓時鍾運行並顯示帶有AddTimerView的 .sheet,則當時鍾啟動時,Picker 正在重置其選擇(檢查記錄)。 我的意圖是讓計時器以 10 毫秒或 1 毫秒的速度運行,而不是 1 秒...當我將計時器更改為 10 毫秒時,我實際上無法與選擇器交互,因為計時器啟動得太快以至於選擇會立即重置。

有誰知道如何擺脫這個問題? 計時器實現是否錯誤或至少不適合多計時器應用程序?

PS1:我注意到當時鍾以 10ms/1ms 運行時,CPU 使用率約為 30%/70%。 此外,當呈現工作表時,CPU 使用率約為 70%/100%。 這是預期的嗎?

PS2:我還注意到,在物理設備上進行測試時,主工具欄中的“+”按鈕並非每次都有效。 我必須滾動計時器列表才能使按鈕再次工作。 這很奇怪:|

在此處輸入圖像描述

還有另一種解決方案。 由於您的計時器計算基於日期之間的差異,因此您可以在 AddTimerView 工作表打開時“暫停” updateTimer function。

將此添加到您的 TimerManager:

@Published var isActive: Bool = true

僅當 isActive 為真時才執行更新:

private func updateTimer(forIndex index: Int) {
    if isActive { // <--- HERE
            
        if self.timers[index].isRunning && !self.timers[index].isPaused {
            self.timers[index].timeElapsed = Date().timeIntervalSince(self.timers[index].startTime ?? Date())
            self.timers[index].remainingPercentage = 1 - self.timers[index].timeElapsed / self.timers[index].duration
                
            if self.timers[index].timeElapsed < self.timers[index].duration {
                let remainingTime = self.timers[index].duration - self.timers[index].timeElapsed
                self.timers[index].displayedTime = remainingTime.asHoursMinutesSeconds
            } else {
                self.stopTimer(self.timers[index])
            }
        }
    }
}

當 AddTimerView 出現或消失時設置 isActive。

NavigationStack {
    .
    .
    .
}
.onAppear{
    tm.isActive = false
}
.onDisappear{
    tm.isActive = true
}

每次你的計時器觸發時,它都需要更新視圖——即重新運行主體代碼,所以它會重新檢查彈出條件。

您可以嘗試幾件事。

一種是將模態展示從工作表切換到

.navigationDestination(isPresented: $helloNewItem, destination: { HelloThereView() })

此外, AddTimerView不應位於其自己的 NavigationStack 中。 MainView中的一個是應該管理子視圖的堆棧。

讓 NavigationStack 負責可能會減少MainViewAddTimerView的干擾。 根據我的經驗,它在背景中比在床單后面的視圖更正式。

另一件事,也許可以更快地測試,是將.id("staticName")添加到AddTimerView演示文稿,這可能會阻止它每次都更新?

.navigationDestination(isPresented: $helloNewItem, destination: { HelloThereView().id("TheOneTimerView") })

更多相關信息: https://swiftui-lab.com/swiftui-id/

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM