Displaying progress from SessionDownloadDelegate with SwiftUI

Viewed 1680

I'm trying to put together a magazine browsing app using SwiftUI, but ran into a snag when trying to create a download progress indicator. I can't seem to find a way to make the ProgressBar I made update from urlSession(didWriteData) in URLSessionDownloadDelegate.

I've got a single DownloadManager which is passed to the main view as an environment object, and which handles the downloading for all magazine issues.

class DownloadManager: NSObject, URLSessionDownloadDelegate, ObservableObject {

    var activeDownloads: [URL: Download] = [:]

    lazy var downloadSession: URLSession = {
        let config = URLSessionConfiguration.default
        return URLSession(configuration: config, delegate: self, delegateQueue: nil)
    }()

    /*downloading and saving the issues is handled here*/

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        guard
            let url = downloadTask.originalRequest?.url,
            let download = activeDownloads[url]
            else {
                return
        }
        //this is the value that needs to be indicated by the UI
        download.progress = (Float(totalBytesWritten) / Float(totalBytesExpectedToWrite))
    }

}

The custom Download class contains the following:

class Download {

    var issue: Issue
    var url: URL

    var progress: Float = 0.0

    var task: URLSessionDownloadTask?
    var resumeData: Data?

    init(issue: Issue) {
        self.issue = issue
        self.url = issue.webLocation
    }

}

The available issues to download are displayed in rows in a List, and while they're being downloaded the progress is supposed to be displayed by the ProgressBar. The relevant portion of the row's layout code is here:

struct IssueRow: View {

    var issue: Issue

    @EnvironmentObject var downloadManager: DownloadManager
    @State var downloadProgress: Float = 0.0

    var body: some View {

        //the progress bar that needs to continuously update
        ProgressBar(value: $downloadProgress)
            .frame(height: 4)

    }

    private func downloadIssue() {
        downloadManager.startDownload(issue: issue)
        downloadProgress = downloadManager.activeDownloads[issue.webLocation]!.progress
        //This does not actually change as the download progresses
    }

Inside ProgressBar, the progress is stored as a @Binding float.

struct ProgressBar: View {

    @Binding var value: Float

    var body: some View {
        GeometryReader { geometry in
            ZStack(alignment: .leading) {
                Rectangle().frame(width: geometry.size.width, height:
                    geometry.size.height)
                    .opacity(0.3)
                    .foregroundColor(Color(UIColor.systemTeal))
                Rectangle().frame(width: CGFloat(self.value)*geometry.size.width, height: geometry.size.height)
                    .foregroundColor(Color.navy)
                    .animation(.linear)
            }
        }
    }
}

I'm really not sure how to synchronize the behavior of SwiftUI and its various elements with the older stuff in Swift. Any help is greatly appreciated!

1 Answers

The approach is to inject progress callback closure in your Download and use it as a bridge.

1) in Download

class Download {

    var issue: Issue
    var url: URL
    var callback: (Float) -> ()    // << here !!

    var progress: Float = 0.0 {
       didSet {
          self.callback(progress)    // << here !!
       }
    }

    // ... other your code here ...
}

2) in SwiftUI

private func downloadIssue() {
    downloadManager.startDownload(issue: issue) { value in // << here !!
      DispatchQueue.main.async {
        self.downloadProgress = value // make sure on main queue
      }
    }
}

3) In startDownload (not provided) pass callback closure into created Download instance, like (just scratchy)

func startDownload(issue: Issue, progress: @escaping (Float) -> ()) {

   let download = Download(issue: issue, url: issue.url, callback: progress)
   ...
}
Related