
This video is only available to subscribers. Start a subscription today to get access to this and 469 other videos.
File Downloads - Part 3
This episode is part of a series: Large File Downloads.
Episode Links
Fixing a Bug From Last Time
Last time I had a bug in the setter for status
, which would cause it to always be nil. The fix is using newValue
in the setter:
class DownloadInfo : NSManagedObject {
var status: DownloadStatus? {
get {
guard let value = statusValue else { return nil }
return DownloadStatus(rawValue: value)
}
set {
statusValue = newValue?.rawValue
}
}
...
}
Rate Limiting Progress Saves
We'd like the model to have as up-to-date information as possible, but calling save
on every progress update would overwhelm the system with many needless writes. Instead, we'll throttle the saves to CoreData by using the RateLimit library.
let rateLimitName = "saveProgress"
if progress == 1.0 {
RateLimit.resetLimitForName(rateLimitName)
}
RateLimit.execute(name: rateLimitName, limit: 0.5) {
print("saving progress...")
try! PersistenceManager.save(context: context)
}
Note that here we force a save if the progress reaches 1.0, which ensures we always save the last update.
Handling Progress Notifications on the Episode List Screen
var progressValues: [Episode : Float] = [:]
override view viewDidLoad() {
...
NotificationCenter.default.addObserver(self, selector: #selector(ViewController.onDownloadProgress(notification:)), name: .downloadProgress, object: nil)
}
func onDownloadProgress(notification: NSNotification) {
guard let episodeID = notification.userInfo?["episodeID"] as? Int,
let progress = notification.userInfo?["progress"] as? Float else {
return
}
guard let episode = getEpisode(byID: episodeID), let indexPath = indexPath(for: episode) else {
return
}
progressValues[episode] = progress
if tableView.indexPathsForVisibleRows?.contains(indexPath) == true {
tableView.reloadRows(at: [indexPath], with: .none)
} else {
print("Row not visible")
}
}
private func getEpisode(byID episodeID: Int) -> Episode? {
return fetchedResultsController?.fetchedObjects?.filter({
return $0.id == Int32(episodeID)
}).first
}
private func indexPath(for episode: Episode) -> IndexPath? {
return fetchedResultsController?.indexPath(forObject: episode)
}
Note that we are saving the progress in the progressValues
dictionary. We do this so that the cell can read from this when it reloads instead of getting this information from the model (which is now stale).
case .Downloading:
let progress = progressValues[episode] ?? episode.downloadInfo?.progress ?? 0
let formattedProgress = String(format: "%.1f%%", progress * 100.0)
return "Downloading... \(formattedProgress)"
Getting Fetched Results Controller To Reload
As we are saving the DownloadInfo
models, these saves don't trigger a delegate callback in the fetched results controller because the objects that match the predicate are not affected directly by the changes we make to DownloadInfo
objects. On download completion, we can force this to trigger a "change" on the episode model by setting the property to itself. This essentially changes nothing, but triggers a flag that will cause the fetched results controller to notify that an object was changed and reload the table:
// trigger a fake change to cause FRC to update the table
downloadInfo.episode?.downloadInfo = downloadInfo