We finish off our operation to import all the episodes given a podcast id and save into the core data store. We also implement a FeedImporter class that listens for new subscriptions in order to kick off the import when a user subscribes.
Creating Context for Import Operation As we complete the refactoring of the SubscriptionStore, we'll now create an instance of the store with a background queue. class ImportEpisodesOperation : BaseOperation { private let podcastId: String private let feedLoader = PodcastFeedLoader() private var context: NSManagedObjectContext! private var subscriptionStore: SubscriptionStore! override func execute() { context = PersistenceManager.shared.newBackgroundContext() subscriptionStore = SubscriptionStore(context: context) } } Loading the Podcast We'll start by loading the podcast ID in the initializer. As this work is entirely done in the background, we'll make sure that we log the details for every step in the console so it is clear what is happening. init(podcastId: String) { self.podcastId = podcastId } override func execute() { context = PersistenceManager.shared.newBackgroundContext() subscriptionStore = SubscriptionStore(context: context) // load podcast print("ImportEpisodes -> Loading podcast: \(podcastId)") guard let podcastEntity = loadPodcast() else { finish() return } } We'll try loading the podcast using podcast id. In case of failure, we'll log the error on the console. private func loadPodcast() -> PodcastEntity? { do { guard let podcast = try subscriptionStore.findPodcast(with: podcastId) else { print("Couldn't find podcast with id: \(podcastId)") return nil } return podcast } catch { print("Error fetching podcast: \(error.localizedDescription)") return nil } } In SubscriptionStore we'll add logic to fetch the podcast using podcast id. func findPodcast(with podcastId: String) throws -> PodcastEntity? { let fetch: NSFetchRequest<PodcastEntity> = PodcastEntity.fetchRequest() fetch.fetchLimit = 1 fetch.predicate = NSPredicate(format: "id == %@", podcastId) return try context.fetch(fetch).first } Importing a Feed To reuse the lookupInfo created for the podcast we'll add this logic in PodcastEntity. var lookupInfo: PodcastLookupInfo? { guard let id = id else { return nil } guard let feedURL = feedURLString.flatMap(URL.init) else { return nil } return PodcastLookupInfo(id: id, feedURL: feedURL) } Now we'll update the MyPodcastsViewController to fetch the podcast. override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let podcast = podcasts[indexPath.row] if let lookup = podcast.lookupInfo { showPodcast(with: lookup) } } We'll also import the feed using lookup info in ImportEpisodesOperation. // import the feed print("ImportEposodes -> Fetching the feed for \(podcastEntity.title ?? "<?>") - \(podcastEntity.feedURLString ?? "<?>")") guard let lookup = podcastEntity.lookupInfo else { print("Couldn't build lookup info") finish() return } feedLoader.fetch(lookup: lookup) { result in switch result { case .failure(let error): print("Error loading feed: \(error.localizedDescription)") self.finish() case .success(let podcast): self.importEpisodes(podcast.episodes, podcast: podcastEntity) // saving the episodes self.finish() } } private func importEpisodes(_ episodes: [Episode], podcast: PodcastEntity) { } Importing the Episodes Once we fetch the episodes, we'll associate the podcast episodes and podcast entity. Here we also make sure not to insert duplicates for an existing episode, instead updating the object that we have. Instead of looping over all the episodes we'll create a lookup for existing episodes by grouping it with episode identifier and then create an episode entity for a new subscribed episode. private func importEpisodes(_ episodes: [Episode], podcast: PodcastEntity) { var existingEpisodes = [String : EpisodeEntity]() podcast.episodes? .map { $0 as! EpisodeEntity } .forEach { existingEpisodes[$0.identifier] = $0 } for episode in episodes { guard let episodeId = episode.identifier else { print("Skipping episode \(episode.title ?? "<?>") because it has no identifier") continue } guard let enclosureURL = episode.enclosureURL else { print("Skipping episode \(episode.title ?? "<?>") because it has no enclosure") continue } let episodeEntity = existingEpisodes[episodeId] ?? EpisodeEntity(context: context) episodeEntity.identifier = episodeId episodeEntity.podcast = podcast episodeEntity.title = episode.title ?? "Untitled" episodeEntity.publicationDate = episode.publicationDate ?? Date() episodeEntity.duration = episode.duration ?? 0 episodeEntity.episodeDescription = episode.description ?? "" episodeEntity.enclosureURL = enclosureURL print("Importing \(episodeEntity.title)...") } } Saving the Episode of a Feed Now we have updated the record in the memory but haven't persisted the record in the Core Data store. feedLoader.fetch(lookup: lookup) { result in switch result { case .failure(let error): print("Error loading feed: \(error.localizedDescription)") self.finish() case .success(let podcast): self.importEpisodes(podcast.episodes, podcast: podcastEntity) self.saveChanges() self.finish() } } private func saveChanges() { context.performAndWait { do { try context.save() } catch { print("Error saving changes: \(error.localizedDescription)") } } } Implementing the Feed Importer To save the subscribed podcast episode from a feed, we'll add a listener for the subscriptions and automatically observer for the notifications. class FeedImporter { static let shared = FeedImporter() private var notificationObserver: NSObjectProtocol? private var importQueue: OperationQueue = { let q = OperationQueue() q.maxConcurrentOperationCount = 2 q.qualityOfService = .userInitiated return q }() func startListening() { notificationObserver = NotificationCenter.default.addObserver(SubscriptionsChanged.self, sender: nil, queue: nil) { notification in notification.subscribedIds.forEach(self.onSubscribe) notification.unsubscribedIds.forEach(self.onUnsubscribe) } } private func onSubscribe(podcastId: String) { let operation = ImportEpisodesOperation(podcastId: podcastId) importQueue.addOperation(operation) } private func onUnsubscribe(podcastId: String) { } } Once the core data is loaded we'll start listening to the FeedImporter. PersistenceManager.shared.initializeModel(then: { FeedImporter.shared.startListening() let storyboard = UIStoryboard(name: "Main", bundle: nil) self.window?.rootViewController = storyboard.instantiateInitialViewController() }) Once we launch the application, we'll subscribe to a podcast and take a look at the path for the core data store displayed in the console. We'll see the episode information saved in the core data database for the recent subscribed podcast.