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!
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)
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With