[英]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 負責可能會減少MainView
對AddTimerView
的干擾。 根據我的經驗,它在背景中比在床單后面的視圖更正式。
另一件事,也許可以更快地測試,是將.id("staticName")
添加到AddTimerView
演示文稿,這可能會阻止它每次都更新?
.navigationDestination(isPresented: $helloNewItem, destination: { HelloThereView().id("TheOneTimerView") })
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.