ProgressView does not complete on long running tasks

My project implements a way of copying files via drag and drop on MacOS. I use the ProgressView to show the progress of the copy operation. Conveniently Progress is used to interact with the NSItemProvider and the ProgressView. On longer running tasks (still seconds) the ProgressView is updated at first, but always stops before completion. The Progress Object has completed on the other hand, but it seems that the last update ist not affected.

I am now using a timer to update the view, but that is somewhat a workaround.

This is the code that is in the view.

struct DragView: View {
    @State private var progress: Progress!

    var body: some View {
         ProgressView(value: progress.fractionCompleted, total: 1)
   }
}

Any hints into how I can get this to work? I am using Xcode 16.2

Answered by SevenSeas in 821902022

For those of you who end up at this question. I use a timer to update the view and then the ProgressView is updated as well.

let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()

...

                            .onReceive(timer) { _ in
                                if let progress = copier?.copyProgress.fractionCompleted {
                                    if progress >= 1 && isCopying {
                                        isCopying = false
                                    }
                                }
                            }

@SevenSeas The code snippet you provided does not show much about your implementation and how the issue can be reproduced. Can you provide a focused sample that can be used to reproduce the issue you described?

Alternatively, the code snippet below tries to simulate what you've described and works just fine. You could use the code snippet below to explain your issue.


struct ContentView: View {
    @State private var progress: Double = 0.0

    var body: some View {
        VStack {
            ProgressView(value: progress, total: 1)
                .progressViewStyle(.linear)

            Button("Start Task") {
                Task {
                    await startTask()
                }
            }
        }
        .onChange(of: progress) {
            dump(progress)
        }
    }

    func startTask() async {
        for i in 0...10 {
            try? await Task.sleep(for: .seconds(1))
            progress = Double(i) / 10.0
        }
    }
}

Thanks for sharing your example. I created a minimal example that shows the behavior I describe.

ContentView:

//
//  ContentView.swift
//  ProgressViewDemo
//
//

import SwiftUI

struct ContentView: View {
    @State var copier: ServerCopyProvider!
    @State var isCopying: Bool = false
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
                .onDrop(of: [.audio, .folder], isTargeted: nil, perform: copyDraggedFiles)
            Text("Hello, world!")
            if isCopying {
                ProgressView(value: copier.copyProgress.fractionCompleted, total: 1)
                    .progressViewStyle(.circular)
            }
        }
        .padding()
    }
    
    func copyDraggedFiles(_ providers: [NSItemProvider], _ point: CGPoint) -> Bool {
        copier = ServerCopyProvider(providers, withAction: { url, error in
            Task {
                try? await Task.sleep(for: .seconds(1))
            }
        })
        isCopying = true
        copier.copyToServer()
        return true
    }
}

#Preview {
    ContentView()
}

ServerCopyProvider

//  ServerCopyProvider.swift
//  MusicConnect
//
//

import Foundation

class ServerCopyProvider: ObservableObject {
    @Published var copyProgress: Progress
    
    let fileAction: (URL?, Error?) -> Void
    let providers: [NSItemProvider]
    
    let LOADUNITCOUNT: Int64 = 1
    let TRANSFERUNITCOUNT: Int64 = 10
    
    init(_ urls: [URL], withAction: @escaping (URL?, Error?) -> Void) {
        let units = Int64(urls.count) * (LOADUNITCOUNT + TRANSFERUNITCOUNT)
        self.copyProgress = Progress(totalUnitCount: units)
        self.fileAction = withAction
        var newProviders: [NSItemProvider] = []
        for url in urls {
            if let provider = NSItemProvider(contentsOf: url) {
                provider.suggestedName = url.lastPathComponent
                newProviders.append(provider)
            }
        }
        self.providers = newProviders
    }
    
    init(_ providers: [NSItemProvider], withAction: @escaping (URL?, Error?) -> Void) {
        let units = Int64(providers.count) * (LOADUNITCOUNT + TRANSFERUNITCOUNT)
        self.copyProgress = Progress(totalUnitCount: units)
        self.fileAction = withAction
        self.providers = providers
    }
    
    func copyToServer() {
        for provider in self.providers {
            provideFileAction(provider, to: fileAction)
        }
    }
    // MARK: - Private functions
    fileprivate func provideFileAction(_ provider: NSItemProvider, to fileAction: @escaping (URL?, Error?) -> Void) {
        let providerIds = ["public.audio", "public.folder", "Public.url"]
        
        for providerId in providerIds {
            if provider.hasItemConformingToTypeIdentifier(providerId) {
                let transferProgress = Progress(totalUnitCount: TRANSFERUNITCOUNT)
                copyProgress.addChild(transferProgress, withPendingUnitCount: TRANSFERUNITCOUNT)
                let loadProgress = provider.loadFileRepresentation(forTypeIdentifier: providerId, completionHandler: self.urlAction(withProgress: transferProgress))
                copyProgress.addChild(loadProgress, withPendingUnitCount: LOADUNITCOUNT)
                return
            }
        }
    }
    
    fileprivate func urlAction(withProgress: Progress) -> ((URL?, Error?) -> Void) {
        return { url, error in
            self.fileAction(url, error)
            withProgress.completedUnitCount = self.TRANSFERUNITCOUNT
            print("\(withProgress.fractionCompleted) - \(self.copyProgress.fractionCompleted)")
        }
    }
}

If you compile it and e.g. drop two files on the globe. The output of the print statement yields: 1.0 - 0.5454545454545454 1.0 - 1.0 But the ProgressView remains at where it was when the progress was updated by the NSItemProvider.

Accepted Answer

For those of you who end up at this question. I use a timer to update the view and then the ProgressView is updated as well.

let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()

...

                            .onReceive(timer) { _ in
                                if let progress = copier?.copyProgress.fractionCompleted {
                                    if progress >= 1 && isCopying {
                                        isCopying = false
                                    }
                                }
                            }
ProgressView does not complete on long running tasks
 
 
Q