diff --git a/TalerCommon/HelperViews/TimeView.swift b/TalerCommon/HelperViews/TimeView.swift index a969250d..1cecc8ff 100644 --- a/TalerCommon/HelperViews/TimeView.swift +++ b/TalerCommon/HelperViews/TimeView.swift @@ -20,8 +20,14 @@ struct TimeView: View { self._isFinished = isFinished } + /// Seconds left, never below zero. + /// + /// The countdown is driven by a TimelineView that keeps ticking while + /// `isFinished` is flipped asynchronously, so without the clamp the view + /// renders "Next retry in: -1 seconds" and keeps counting down into the + /// negatives until the state change lands. func remainingTime(_ elapsed: Int) -> Int { - let remaining = seconds - elapsed + let remaining = max(0, seconds - elapsed) if remaining <= 0 { if isFinished != nil { DispatchQueue.main.async { diff --git a/TalerWallet1/Views/HelperViews/LoadingView.swift b/TalerWallet1/Views/HelperViews/LoadingView.swift index 01fd9859..99e2a5ab 100644 --- a/TalerWallet1/Views/HelperViews/LoadingView.swift +++ b/TalerWallet1/Views/HelperViews/LoadingView.swift @@ -64,17 +64,25 @@ struct StalledView: View { if let lastPhase = controller.lastProgressPhase { if lastPhase.phase == .stalled { if let lastError, let delay = lastError.nextRetryDelay { + // The three states cross-fade, so all of them stay in + // the hierarchy. opacity(0) hides a view from sight + // but not from VoiceOver, which otherwise reads all + // three at once: "Next retry in: 3 seconds", + // "Retrying...", "Waiting for response...". ZStack { TimeView("Next retry in:", seconds: Int(delay.toSeconds()), isFinished: $isFinished) .talerFont(.title3) .padding(.vertical) .opacity(isFinished == false ? 1 : 0) + .accessibilityHidden(isFinished != false) Text("Retrying…") .talerFont(.title3) .opacity(isFinished == true ? 1 : 0) + .accessibilityHidden(isFinished != true) Text("Waiting for response…") .talerFont(.title3) .opacity(isFinished == nil ? 1 : 0) + .accessibilityHidden(isFinished != nil) } retryButton(disabled: isFinished ?? false) }