A parte central do meu App é um timer. Até agora, este foi o Código (retirado do Codebase principal para ser o mínimo reproduzível):
import SwiftUI
struct TestView: View {
@State var countdownTimer = 5 * 60 /// The actual seconds of the timer, being counted down/up
@State var timerRunning = false /// Var to set and see if timer is running
var body: some View {
/// For formatting the timer numbers
let style = Duration.TimeFormatStyle(pattern: .minuteSecond)
let formTimer = Duration.seconds(countdownTimer).formatted()
let formTimerShort = Duration.seconds(countdownTimer).formatted(style)
/// The main Timer If there are no hours we hide the hour number and make the timer bigger in this way.
Text(countdownTimer / 3600 == 0 && timerRunning ? "\(formTimerShort)" : "\(formTimer)")
.onReceive(Timer.publish(every: 1, on: .main, in: .common).autoconnect()) { _ in
if countdownTimer > 0 && timerRunning {
countdownTimer -= 1
} else {
timerRunning = false
}
}
/// The Start and Pause buttons
HStack {
if !timerRunning {
Button(action: {
timerRunning = true
}) {
Text("Start")
.frame(width: 300, height: 75)
.font(.system(size: 60))
}
} else {
Button(action: {
timerRunning = false
}) {
Text("Pause")
.frame(width: 300, height: 75)
.font(.system(size: 60))
}
}
}
}
}
#Preview {
TestView()
}
O problema com isso era que não era preciso. Cerca de 40 segundos em 1 hora de uso do timer. Então minha ideia era mudar quando o Timer é publicado de 1 para 0,001, aumentando assim a resolução. Este Código funciona, mas é ainda menos preciso!
struct TestView: View {
@State var countdownTimer = 5 * 60 /// The actual seconds of the timer, being counted down/up
@State private var fractionTimer = 0 /// Helping us to make the timer more accurate
@State var timerRunning = false /// Var to set and see if timer is running
var body: some View {
[...]
/// The main Timer If there are no hours we hide the hour number and make the timer bigger in this way.
Text(countdownTimer / 3600 == 0 && timerRunning ? "\(formTimerShort)" : "\(formTimer)")
.onReceive(Timer.publish(every: 0.001, on: .main, in: .common).autoconnect()) { _ in
/// Count up the fraction timer, only if he hits a certain Number we count up our Visible Timer
if timerRunning {
fractionTimer += 1
}
if fractionTimer == 1000 {
if countdownTimer > 0 && timerRunning {
countdownTimer -= 1
fractionTimer = 0
} else {
timerRunning = false
fractionTimer = 0
}
}
}
[...]
}
}
De acordo com minha lógica, isso deveria ter tirado a imprecisão de 40 segundos por hora para 0,04 segundos por hora. Mas, na verdade, agora está 120 segundos por hora fora!
Como isso pode ser? É porque ele está rodando no thread principal e porque ele é chamado 1000 vezes por segundo, há muitas outras tarefas que atrasam essa chamada? Devo tirá-lo do thread principal?
Não trabalhei muito com timers no SwiftUI, mas, em geral, você não quer depender de timers para lhe dar o número exato de chamadas que você pede. (Eles ocasionalmente "pulam uma batida" se o sistema estiver ocupado no momento em que o timer deveria disparar.) Você deve usar um
Timer
para atualizar sua UI e, em seguida, usar matemática com o objeto currentDate
e um endDate
para calcular a quantidade de tempo decorrido. (Então, volte para um timer que dispara uma vez por segundo, ou algumas vezes por segundo, mas calcule o número de segundos restantes com base no currentDate
cada vez que o timer dispara.)Considere esta variação do seu código, que não perderá segundos:
A imprecisão é provavelmente porque o timer é perdido toda vez que dispara e o problema é agravado pelo uso indevido de onChange. Para evitar que ele seja perdido, o publicador do timer precisa ser um estado, por exemplo
onChange é para ações externas, não para atualizar o estado. O estado já deve ter o valor correto antes de body ser chamado.
Além disso, o if dentro do corpo quebra a identidade do SwiftUIs, causando alocações extras de heap e o deixando lento. Você deve ter uma única declaração de Button e usar if dentro de seus parâmetros.