Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Displaying progress from SessionDownloadDelegate with SwiftUI

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!

like image 881
iegj33 Avatar asked Jan 26 '23 01:01

iegj33


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)
   ...
}
like image 139
Asperi Avatar answered Apr 09 '23 19:04

Asperi