Search code examples
swiftswiftuiprogress-barcombine

ProgressBar iOS 13


I've tried creating my own ProgressView to support iOS 13, but for some reason it appears to not work. I've tried @State, @Binding and the plain var progress: Progress, but it doesn't update at all.

struct ProgressBar: View {
    @Binding var progress: Progress

    var body: some View {
        VStack(alignment: .leading) {
            Text("\(Int(progress.fractionCompleted))% completed")
            ZStack {
                RoundedRectangle(cornerRadius: 2)
                    .foregroundColor(Color(UIColor.systemGray5))
                    .frame(height: 4)
                GeometryReader { metrics in
                    RoundedRectangle(cornerRadius: 2)
                        .foregroundColor(.blue)
                        .frame(width: metrics.size.width * CGFloat(progress.fractionCompleted))
                }
            }.frame(height: 4)
            Text("\(progress.completedUnitCount) of \(progress.totalUnitCount)")
                .font(.footnote)
                .foregroundColor(.gray)
        }
    }
}

In my content view I added both the iOS 14 variant and the iOS 13 supporting one. They look the same, but the iOS 13 variant does not change anything.

struct ContentView: View {
    @State private var progress = Progress(totalUnitCount: 10)
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        VStack {
            ProgressBar(progress: $progress)
                .padding()
                .onReceive(timer) { timer in
                    progress.completedUnitCount += 1
                    if progress.isFinished {
                        self.timer.upstream.connect().cancel()
                        progress.totalUnitCount = 500
                    }
                }
            if #available(iOS 14, *) {
                ProgressView(progress)
                    .padding()
                    .onReceive(timer) { timer in
                        progress.completedUnitCount += 1
                        if progress.isFinished {
                            self.timer.upstream.connect().cancel()
                            progress.totalUnitCount = 500
                        }
                    }
            }
        }
    }
}

The iOS 14 variant works, but my iOS 13 implementation fails. Can somebody help me?


Solution

  • Progress is-a NSObject, it is not a struct, so state does not work for it. You have to use KVO to observe changes in Progress and redirect into SwiftUI view's source of truth.

    Here is a simplified demo of possible solution. Tested with Xcode 12.1 / iOS 14.1

    demo

    class ProgressBarViewModel: ObservableObject {
        @Published var fractionCompleted: Double
        let progress: Progress
        private var observer: NSKeyValueObservation!
        
        init(_ progress: Progress) {
            self.progress = progress
            self.fractionCompleted = progress.fractionCompleted
            observer = progress.observe(\.completedUnitCount) { [weak self] (sender, _) in
                self?.fractionCompleted = sender.fractionCompleted
            }
        }
    }
    
    struct ProgressBar: View {
        @ObservedObject private var vm: ProgressBarViewModel
        
        init(_ progress: Progress) {
            self.vm = ProgressBarViewModel(progress)
        }
        
        var body: some View {
            VStack(alignment: .leading) {
                Text("\(Int(vm.fractionCompleted * 100))% completed")
                ZStack {
                    RoundedRectangle(cornerRadius: 2)
                        .foregroundColor(Color(UIColor.systemGray5))
                        .frame(height: 4)
                    GeometryReader { metrics in
                        RoundedRectangle(cornerRadius: 2)
                            .foregroundColor(.blue)
                            .frame(width: metrics.size.width * CGFloat(vm.fractionCompleted))
                    }
                }.frame(height: 4)
                Text("\(vm.progress.completedUnitCount) of \(vm.progress.totalUnitCount)")
                    .font(.footnote)
                    .foregroundColor(.gray)
            }
        }
    }
    

    and updated call place to use same constructor

    struct ContentView: View {
        @State private var progress = Progress(totalUnitCount: 10)
        let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
        var body: some View {
            VStack {
                ProgressBar(progress)      // << here !!
    
    // ... other code