My podcast app called PeaPodder uses a complex algorithm to scan for new content. This post is about an attempt to simplify this code using the async/await syntax that Apple added to Swift (in 2021 I believe)
For a description of the intentions behind this syntax and how to use it, I’d recommend starting with this video.
PeaPodder has a list of subscribed podcasts, and needs to periodically scan for new content. The algorithm works something like this:
- Set a state flag scanning to true
- For the first podcast, do the following:
- Does it already have more than 120 minutes of new content? If so, go back to step 2 and start scanning the next podcast
- If not, are there any episodes that are known, but not yet downloaded? If there are, proceed to step 5, otherwise, jump to step 6
- Start a network request to fetch the first known episode, with a delegate to handle completion and/or errors, and exit this scan
- If there are no known episodes, check if is appropriate1 to query ListenNotes to see if there are new episodes for the current podcast. If not, go back to step 2 with the next podcast
- Start to fetch to see if the podcast has any new episodes, using a delegate to handle completion and/or errors. Also, exit this scan
The delegate mentioned in step 5 will receive callbacks to either add the new episode to the app data store, or report the download area. After this adding/reporting is done, it will restart the scanning process, from the beginning.
Similarly the delegate mentioned in step 7 will either successfully fetch the podcast metadata or report the fetch error. After this is done, it will restart the scanning process, again from the very beginning.
I didn’t love this algorithm for a couple of reasons.
Reason 1, starting at the top of the list each time it comes back from a fetch just feels embarrassingly clunky. (inelegant?) But given how much time could elapse waiting for an episode to download, it is possible the user could modify the original list of podcasts. Given that possibility, this approach feels like the simplest ‘safe’ way to perform this task
Reason 2 is the code complexity required to ensure all the callbacks handle all the cases correctly. The Apple video mentioned above includes a relatively simple example of this complexity. This code gets even more complex because both the podcast metadata fetch and the episode download can be performed outside the automatic scan. This means the fetch code needs to work correctly both using the automatic scanning context, and also a user triggered context.
The async/await version of scan does the following:
- Iterate through all the podcasts
- Does the current podcast have enough minutes? If so, go back to step 1 with the next podcast
- If not, are there any known, unplayed episodes? If so download one or more of them either until they are all downloaded, or the podcast has enough minutes of content.
- If the content limit has not yet been reached, check if it is appropriate to query ListenNotes to see if the podcast has any new episodes
- If the query gets sent, and returns with details about one or more new episodes, download it/them, stopping if/when the podcast content limit is reached
- Go to the next podcast
All this gets done in one top to bottom code path. The functions that perform the network requests can be reused for fetches that occur outside the scanning process, without any additional code. Errors get handled where they occur. Depending on the error, the scan may either continue, or abort/throw. And the intention will be clear from looking at the code.
Unfortunately my testing uncovered a PLOT TWIST. The URLSession calls that can be used in async tasks cannot be used to perform background fetches. Background configured URLSessions can only use the functions with delegate callbacks. In theory, PeaPodder could use foreground fetches, but it would mean users would need to keep the app running in the foreground while the fetching and downloading is happening.
So I learned a lot. I now feel quite comfortable working with async functions. I also feel I improved my understanding of the inner working of URLSession. Also I’ve shone a bright light on a portion of the PeaPodder app that had become a mysterious black hole to me. Sadly the podcast scanning function still has some gnarly complicated code.
- Peapodder uses the free tier of service at ListenNotes, which limits monthly use to 1000 calls to the API. I’ve used a couple of different techniques to minimize the number of calls that get made, while maximizing the app’s ability to fetch episodes in a timely manner. I’m debating whether this might be the topic for another post. ↩︎
