import Combine import SwiftUI /// A ProgressIndicator that reflects the state of a Progress object. /// This functionality is already built in to ProgressView, /// but this implementation ensures that changes are received on the main thread. @available(iOS 14.0, macOS 11.0, *) public struct ObservingProgressIndicator: View { let controlSize: NSControl.ControlSize let style: NSProgressIndicator.Style let showsAdditionalDescription: Bool @StateObject private var progress: ProgressWrapper public init( _ progress: Progress, controlSize: NSControl.ControlSize, style: NSProgressIndicator.Style, showsAdditionalDescription: Bool = false ) { _progress = StateObject(wrappedValue: ProgressWrapper(progress: progress)) self.controlSize = controlSize self.style = style self.showsAdditionalDescription = showsAdditionalDescription } class ProgressWrapper: ObservableObject { var progress: Progress var cancellable: AnyCancellable! init(progress: Progress) { self.progress = progress cancellable = progress.publisher(for: \.fractionCompleted) .combineLatest(progress.publisher(for: \.localizedAdditionalDescription)) .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true) .sink { [weak self] _ in self?.objectWillChange.send() } } } public var body: some View { VStack(alignment: .leading, spacing: 0) { ProgressIndicator( minValue: 0.0, maxValue: 1.0, doubleValue: progress.progress.fractionCompleted, controlSize: controlSize, isIndeterminate: progress.progress.isIndeterminate, style: style ) .help(String(format: localizeString("DownloadingPercentDescription"), Int((progress.progress.fractionCompleted * 100)))) if showsAdditionalDescription, progress.progress.xcodesLocalizedDescription.isEmpty == false { Text(progress.progress.xcodesLocalizedDescription) .font(.subheadline.monospacedDigit()) .foregroundColor(.secondary) } } } } @available(iOS 14.0, macOS 11.0, *) struct ObservingProgressBar_Previews: PreviewProvider { static var previews: some View { Group { ObservingProgressIndicator( configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 }, controlSize: .small, style: .spinning ) ObservingProgressIndicator( configure(Progress()) { $0.kind = .file $0.fileOperationKind = .downloading $0.estimatedTimeRemaining = 123 $0.totalUnitCount = 11944848484 $0.completedUnitCount = 848444920 $0.throughput = 9211681 }, controlSize: .regular, style: .bar, showsAdditionalDescription: true ) } .previewLayout(.sizeThatFits) } }