ProgressIndicatorView.swift 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. //
  2. // ProgressIndicatorView.swift
  3. // DashKitUI
  4. //
  5. // Created by Pete Schwamb on 3/2/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import SwiftUI
  9. import Combine
  10. public enum ProgressIndicatorState: Equatable {
  11. case hidden
  12. case indeterminantProgress
  13. case timedProgress(finishTime: CFTimeInterval)
  14. case completed
  15. }
  16. extension ProgressIndicatorState {
  17. var showProgressBar: Bool {
  18. if case .timedProgress = self {
  19. return true
  20. }
  21. return false
  22. }
  23. var showIndeterminantProgress: Bool {
  24. if case .indeterminantProgress = self {
  25. return true
  26. }
  27. return false
  28. }
  29. var showCompletion: Bool {
  30. if case .completed = self {
  31. return true
  32. }
  33. return false
  34. }
  35. }
  36. public struct ProgressIndicatorView: View {
  37. private let state: ProgressIndicatorState
  38. private let fullSize: CGFloat = 35
  39. // timed progress
  40. private let timer: Publishers.Autoconnect<Timer.TimerPublisher>
  41. @State private var progress: Double = 0
  42. private let startTime: CFTimeInterval
  43. private var finishTime: CFTimeInterval
  44. private var duration: TimeInterval {
  45. return max(0, finishTime - startTime)
  46. }
  47. public init(state: ProgressIndicatorState) {
  48. startTime = CACurrentMediaTime()
  49. self.state = state
  50. timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
  51. if case .timedProgress(let finishTime) = state {
  52. self.finishTime = finishTime
  53. } else {
  54. timer.upstream.connect().cancel()
  55. self.finishTime = startTime
  56. }
  57. }
  58. public var body: some View {
  59. ZStack {
  60. ActivityIndicator(isAnimating: .constant(true), style: .large)
  61. .opacity(self.state.showIndeterminantProgress ? 1 : 0)
  62. .frame(height: self.state.showIndeterminantProgress ? fullSize : 0)
  63. ZStack {
  64. ProgressView(progress: self.state.showProgressBar ? 1 : 0)
  65. .frame(height: fullSize)
  66. .animation(.linear(duration: self.duration))
  67. }
  68. .opacity(self.state.showProgressBar ? 1 : 0)
  69. .frame(height: self.state.showProgressBar ? fullSize : 0)
  70. Image(frameworkImage: "Checkmark").foregroundColor(Color.accentColor)
  71. .opacity(self.state.showCompletion ? 1 : 0)
  72. .scaleEffect(self.state.showCompletion ? 1.0 : 0.001)
  73. .animation(.spring(dampingFraction: 0.5))
  74. .frame(height: self.state.showCompletion ? fullSize : 0)
  75. }
  76. .accessibilityElement(children: .ignore)
  77. .accessibility(label: Text(self.accessibilityLabel))
  78. .accessibility(hidden: self.state == .hidden)
  79. .onReceive(timer) { time in
  80. let elapsed = CACurrentMediaTime() - self.startTime
  81. self.progress = min(1.0, elapsed / self.duration)
  82. if self.progress >= 1.0 {
  83. self.timer.upstream.connect().cancel()
  84. }
  85. }
  86. }
  87. var accessibilityLabel: String {
  88. switch self.state {
  89. case .indeterminantProgress:
  90. return LocalizedString("Progressing.", comment: "Accessibility label for ProgressIndicatorView when showIndeterminantProgress")
  91. case .timedProgress:
  92. return String(format: LocalizedString("%1$d percent complete.", comment: "Format string for progress accessibility label (1: duration in seconds)"), Int((self.progress * 100).rounded()))
  93. case .completed:
  94. return LocalizedString("Completed.", comment: "Accessibility label for ProgressIndicatorView when showIndeterminantProgress")
  95. case .hidden:
  96. return ""
  97. }
  98. }
  99. }
  100. struct ProgressIndicatorView_Previews: PreviewProvider {
  101. static var previews: some View {
  102. ProgressPreviewWrapper()
  103. }
  104. }
  105. struct ProgressPreviewWrapper: View {
  106. @State var setupState: ProgressIndicatorState = .hidden
  107. @State private var modeIndex: Int = 0
  108. var body: some View {
  109. VStack {
  110. Rectangle().frame(height: 1)
  111. ProgressIndicatorView(state: setupState)
  112. Rectangle().frame(height: 1)
  113. Button(action: {
  114. let finishTime = TimeInterval(10)
  115. let modes: [ProgressIndicatorState] = [.indeterminantProgress, .timedProgress(finishTime: CACurrentMediaTime() + finishTime), .completed, .hidden]
  116. self.setupState = modes[self.modeIndex]
  117. self.modeIndex = (self.modeIndex + 1) % modes.count
  118. }) {
  119. Text("Switch Preview State")
  120. }
  121. Text(String(describing: self.setupState)).foregroundColor(Color.secondary).lineLimit(1)
  122. }
  123. .animation(.default)
  124. .padding()
  125. }
  126. }