Files
divkit/client/ios/DivKit/Timers/DivTimerController.swift
pkurchatov e3e2ceca7d Fixed timers behavior
commit_hash:e887cde072bcbf75ba2c92747f9311f21eb36eec
2026-05-29 11:39:49 +03:00

278 lines
7.2 KiB
Swift

import Foundation
import VGSL
final class DivTimerController {
typealias RunActions = ([DivAction]) -> Void
typealias UpdateCard = () -> Void
enum State {
case started
case stopped
case paused
}
private(set) var state: State = .stopped
private let cardId: DivCardID
private let divTimer: DivTimer
private let timerScheduler: Scheduling
private let timeMeasuring: TimeMeasuring
private let runActions: RunActions
private let updateCard: UpdateCard
private let variablesStorage: DivVariablesStorage
private let functionsStorage: DivFunctionsStorage
private let persistentValuesStorage: DivPersistentValuesStorage
private let reporter: DivReporter
private var savedDuration: TimeInterval?
private var savedInterval: TimeInterval?
private var tickTimer: TimerType?
private var endTimer: TimerType?
private var expectedTickCount: Int = 0
private var actualTickCount: Int = 0
private var tickBeforeEndIsNeeded = false
init(
cardId: DivCardID,
divTimer: DivTimer,
timerScheduler: Scheduling,
timeMeasuring: TimeMeasuring,
runActions: @escaping RunActions,
updateCard: @escaping UpdateCard,
variablesStorage: DivVariablesStorage,
functionsStorage: DivFunctionsStorage,
persistentValuesStorage: DivPersistentValuesStorage,
reporter: DivReporter
) {
self.cardId = cardId
self.divTimer = divTimer
self.timerScheduler = timerScheduler
self.timeMeasuring = timeMeasuring
self.runActions = runActions
self.updateCard = updateCard
self.variablesStorage = variablesStorage
self.functionsStorage = functionsStorage
self.persistentValuesStorage = persistentValuesStorage
self.reporter = reporter
}
deinit {
cancel()
}
func start() {
if state != .stopped {
DivKitLogger.error("Timer '\(divTimer.id)' can't start because it has state '\(state)'.")
return
}
let expressionResolver = ExpressionResolver(
path: cardId.path,
variablesStorage: variablesStorage,
functionsStorage: functionsStorage,
persistentValuesStorage: persistentValuesStorage,
reporter: reporter
)
guard divTimer.parametersAreValid(expressionResolver) else {
DivKitLogger.error("Timer '\(divTimer.id)' is not valid.")
return
}
state = .started
if let duration = divTimer.getDuration(expressionResolver),
let tickInterval = divTimer.getTickInterval(expressionResolver) {
expectedTickCount = duration / tickInterval
tickBeforeEndIsNeeded = duration % tickInterval == 0
} else {
expectedTickCount = 0
tickBeforeEndIsNeeded = false
}
savedDuration = divTimer.getDuration(expressionResolver)?.toTimeInterval
savedInterval = divTimer.getTickInterval(expressionResolver)?.toTimeInterval
actualTickCount = 0
setVariable(0)
timeMeasuring.start()
makeTickTimer()
makeEndTimer()
}
func stop() {
if state == .stopped {
DivKitLogger.error("Timer '\(divTimer.id)' already stopped.")
return
}
state = .stopped
invalidateTimers()
if let endActions = divTimer.endActions {
runActions(endActions)
}
updateCard()
}
func pause() {
if state != .started {
DivKitLogger.error("Timer '\(divTimer.id)' can't pause because it has state '\(state)'.")
return
}
state = .paused
timeMeasuring.pause()
setVariable(timeMeasuring.passedInterval())
invalidateTimers()
}
func resume() {
if state != .paused {
DivKitLogger.error("Timer '\(divTimer.id)' can't resume because it has state '\(state)'.")
return
}
state = .started
makeRemainderTickTimer()
makeEndTimer()
timeMeasuring.resume()
}
func cancel() {
state = .stopped
invalidateTimers()
}
func reset() {
cancel()
start()
}
private func makeEndTimer() {
guard let duration = savedDuration else {
return
}
let delay = duration - timeMeasuring.passedInterval()
guard delay > 0 else {
return
}
endTimer = timerScheduler.makeTimer(
delay: delay,
handler: { [weak self] in
self?.onEnd(duration: duration)
}
)
}
private func makeRemainderTickTimer() {
guard let tickInterval = savedInterval, tickInterval > 0 else {
return
}
let remainder = timeMeasuring.passedInterval().truncatingRemainder(dividingBy: tickInterval)
guard remainder > ACCURACY else {
makeTickTimer()
return
}
tickTimer = timerScheduler.makeTimer(
delay: tickInterval - remainder,
handler: { [weak self] in
self?.makeTickTimer()
self?.onTick()
}
)
}
private func makeTickTimer() {
guard let tickInterval = savedInterval, tickInterval > 0 else {
return
}
tickTimer = timerScheduler.makeRepeatingTimer(
interval: tickInterval,
handler: { [weak self] in
self?.onTick()
}
)
}
private func onTick() {
guard state == .started else {
return
}
let passedInterval = timeMeasuring.passedInterval()
if let duration = savedDuration, passedInterval >= duration {
return
}
actualTickCount += 1
setVariable(passedInterval)
if let tickActions = divTimer.tickActions {
runActions(tickActions)
}
updateCard()
}
private func onEnd(duration: TimeInterval) {
guard state == .started else {
return
}
setVariable(duration)
if tickBeforeEndIsNeeded, actualTickCount < expectedTickCount {
if let tickActions = divTimer.tickActions {
runActions(tickActions)
}
}
stop()
}
private func setVariable(_ value: TimeInterval) {
if let variableName = divTimer.valueVariable {
variablesStorage.update(
cardId: cardId,
name: DivVariableName(rawValue: variableName),
value: String(format: "%.0f", value * 1000)
)
}
}
private func invalidateTimers() {
tickTimer?.invalidate()
endTimer?.invalidate()
tickTimer = nil
endTimer = nil
}
}
extension DivTimer {
fileprivate func getDuration(_ expressionResolver: ExpressionResolver) -> Int? {
let duration = resolveDuration(expressionResolver)
guard duration > 0 else {
return nil
}
return duration
}
fileprivate func getTickInterval(_ expressionResolver: ExpressionResolver) -> Int? {
guard let tickInterval = resolveTickInterval(expressionResolver),
tickInterval > 0 else {
return nil
}
return tickInterval
}
fileprivate func parametersAreValid(_ expressionResolver: ExpressionResolver) -> Bool {
let duration = resolveDuration(expressionResolver)
if duration > 0 {
return true
}
guard let tickInterval = resolveTickInterval(expressionResolver), tickInterval > 0 else {
DivKitLogger.error("Timer '\(self.id)' parameters is not valid: tick_interval not set")
return false
}
if tickActions == nil, valueVariable == nil {
DivKitLogger.error(
"Timer '\(self.id)' parameters is not valid: set tickActions or valueVariable"
)
return false
}
return true
}
}
extension Int {
var toTimeInterval: TimeInterval {
TimeInterval(Double(self) / 1000)
}
}
private let ACCURACY = 0.01