File Downloads - Part 3

Episode #241 | 17 minutes | published on October 27, 2016
In this episode we take the download state and progress notifications and update the user interface to reflect this state. We'll see how to translate the notification into the indexPath for that episode row, and how to fake a change related to the fetched results controller to trigger a reload of the content. We will leverage the RateLimit library to save periodic changes in progress to the model without overwhelming Core Data.

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
blog comments powered by Disqus