Thanks for the advice!
Informally, I've generally found that a progress bar needs to update every ~0.1s (10x/sec) to ~1-2s in order to look "right".
I guess this depends on the magnitude of the task. If the task takes an hour, then updating every 1s would mean that each update moves the progress bar by a fraction of a pixel. My current setup moves the progress bar about 4px at a time, which looks pretty smooth to me.
You can use the progress delegate that provides "raw" rate data; however, the easier option is to use the URLSessionTask.Progress object. With single downloads, you can use that Progress object directly, while more complicated scenarios can use child progress and the flow described here.
Thanks for the pointers. I looked into these options but, in the end, it feels like trying to tie the progress to bytes downloaded (rather than number of files completed) is going to open up other issues, notably:
If a file fails to download for some reason (and has to be restarted), I will need to subtract out the bytes downloaded from the progress or, in some sense, track each file's progress. Not impossible, but feels fraught with state management bug potential.
My task involves downloading and processing files. In some cases, the processing is very small relative to the download time, but in other cases, the processing can take longer than the download, so linking progress only to the download part is not ideal. That's why I originally chose to link progress to file completion.
The modifications I'd need to make to my code (passing download progress from the URLSession delegate through an AsyncStream back to the calling task) will result in more complexity that will be hard to test, and background URLSessions are already really hard to work with.
Overall, my sense is that BGContinuedProcessingTask is better suited to tasks that are primarily CPU-bound where the rate of progress and time-to-completion is fairly predictable a-priori. For tasks that are more network-bound and where progress is heavily dependent on the volume of user data, it becomes hard to guarantee a particular rate of progress.
Since BGContinuedProcessingTask seems to be quite trigger happy and doesn't distinguish between user cancellation and system cancellation, I don't think it's going to be the right option for me. Which is a shame because I was quite excited about it when I saw it at WWDC.
Anyway, thanks for the advice, Kevin, and for being very responsive (also to other threads on this forum). It's much appreciated!