mirror of
https://github.com/ProtonMail/ios-mail.git
synced 2026-05-15 09:50:39 +00:00
111 lines
3.5 KiB
Swift
111 lines
3.5 KiB
Swift
import InboxCore
|
|
import SwiftUI
|
|
|
|
@MainActor
|
|
final class LoadingBarStateStore: ObservableObject {
|
|
enum Action {
|
|
case startLoading
|
|
case stopLoading
|
|
case cycleCompleted
|
|
case barVisibilityChanged(isVisible: Bool)
|
|
}
|
|
|
|
@Published private(set) var isLoading = false
|
|
|
|
private let configuration: LoadingBarConfiguration
|
|
private var completedCycles = 0
|
|
private var stopRequested = false
|
|
private var startDate: Date?
|
|
private var targetCyclesAfterStop: Int?
|
|
private var isVisible = false
|
|
|
|
init(configuration: LoadingBarConfiguration) {
|
|
self.configuration = configuration
|
|
}
|
|
|
|
func handle(action: Action) {
|
|
switch action {
|
|
case .barVisibilityChanged(let isVisible):
|
|
self.isVisible = isVisible
|
|
case .startLoading:
|
|
guard !isLoading && isVisible else { return }
|
|
isLoading = true
|
|
completedCycles = 0
|
|
stopRequested = false
|
|
startDate = DateEnvironment.currentDate()
|
|
targetCyclesAfterStop = nil
|
|
case .stopLoading:
|
|
guard isLoading, let start = startDate else {
|
|
stopLoading()
|
|
return
|
|
}
|
|
|
|
stopRequested = true
|
|
|
|
let currentDate = DateEnvironment.currentDate()
|
|
|
|
if shouldStopNowIfOnBoundary(now: currentDate, start: start) {
|
|
stopLoading()
|
|
return
|
|
}
|
|
|
|
let elapsedTimeInSeconds = currentDate.timeIntervalSince(start)
|
|
let requiredCycles = requiredCycles(elapsedTimeInSeconds: elapsedTimeInSeconds)
|
|
|
|
targetCyclesAfterStop = max(requiredCycles, completedCycles)
|
|
case .cycleCompleted:
|
|
guard isLoading else { return }
|
|
|
|
completedCycles += 1
|
|
|
|
guard stopRequested else { return }
|
|
|
|
if let targetCycles = targetCyclesAfterStop, completedCycles >= targetCycles {
|
|
stopLoading()
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func stopLoading() {
|
|
isLoading = false
|
|
completedCycles = 0
|
|
stopRequested = false
|
|
startDate = nil
|
|
targetCyclesAfterStop = nil
|
|
}
|
|
|
|
private func requiredCycles(elapsedTimeInSeconds: TimeInterval) -> Int {
|
|
let cycleDuration = configuration.cycleDuration
|
|
let tolerance = configuration.tolerance
|
|
|
|
let requiredCycles = max(1, Int(ceil(elapsedTimeInSeconds / cycleDuration)))
|
|
let remainingTimeInCurrentCycle = Double(requiredCycles) * cycleDuration - elapsedTimeInSeconds
|
|
|
|
let needExtraCycle =
|
|
tolerance > 0 && remainingTimeInCurrentCycle > 0 && remainingTimeInCurrentCycle < tolerance && elapsedTimeInSeconds >= cycleDuration
|
|
let extraCycle = needExtraCycle ? 1 : 0
|
|
|
|
return requiredCycles + extraCycle
|
|
}
|
|
|
|
private func shouldStopNowIfOnBoundary(now: Date, start: Date) -> Bool {
|
|
let elapsed = now.timeIntervalSince(start)
|
|
let cycleDuration = configuration.cycleDuration
|
|
|
|
/// Must complete at least one cycle before allowing immediate stop
|
|
guard elapsed >= cycleDuration else {
|
|
return false
|
|
}
|
|
|
|
let elapsedCycles = elapsed / cycleDuration
|
|
let fractionalPart = elapsedCycles - floor(elapsedCycles)
|
|
|
|
let isNearCycleStart = fractionalPart < configuration.fractionalTolerance
|
|
let isNearCycleEnd = (1.0 - fractionalPart) < configuration.fractionalTolerance
|
|
|
|
return isNearCycleStart || isNearCycleEnd
|
|
}
|
|
}
|