In this episode we set up a Core Data model for persisting podcast subscriptions. We'll cover the various ways Xcode generates model classes for us and work on saving and loading podcast subscriptions so that the subscribe button behaves as it should.
Episode Links Base - SQLite App Creating a Core Data Model We will create a new Core Data model Subscriptions with podcast and subscription as entities. We will add a few attributes to represent podcasts with id and feedURLString as non-optional. Also, we will add a date attribute to subscription entity which will be used to track the subscriptions for the podcast. To have a one-to-one relationship between the subscription for one podcast we will create an inverse relationship between podcast and subscription. We will set the cascade delete rule for this relationship. This will ensure the deletion of subscription if a podcast is deleted, or the deletion of the podcast while a subscription is deleted. To add the custom methods and classes we will set the code generator as "Category/Extension". Next we will create an NSManagedObject subclass for subscription model with our podcast and subscription entities. Saving a Subscription Now we will create an object to save our subscriptions. We will create a static shared instance of the store and a persistent container to initialize it. class SubscriptionStore { static var shared = SubscriptionStore() private let persistentContainer: NSPersistentContainer var mainContext: NSManagedObjectContext { return persistentContainer.viewContext } private init() { persistentContainer = NSPersistentContainer(name: "Subscriptions") } func initializeModel() { persistentContainer.loadPersistentStores { (storeDescription, error) in if let error = error { fatalError("Core Data error: \(error.localizedDescription)") } else { print("Loaded Store: \(storeDescription.url?.absoluteString ?? "nil")") } } } } Initializing the Data Model After creating our SubscriptionStore model, we will initialize it in AppDelegate. func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { Theme.apply(to: window!) SubscriptionStore.shared.initializeModel() return true } Setting the Initial Stage To set the initial stage of the subscription button while we load a podcast, we will create a view model which will set the data for the subscribed podcast and also helps in wrapping the podcast episode. struct PodcastViewModel { private let podcast: Podcast let isSubscribed: Bool init(podcast: Podcast, isSubscribed: Bool) { self.podcast = podcast self.isSubscribed = isSubscribed } var title: String? { return podcast.title } var author: String? { return podcast.author } var genre: String? { return podcast.primaryGenre } var description: String? { return podcast.description? .strippingHTML() .trimmingCharacters(in: .whitespacesAndNewlines) } var artworkURL: URL? { return podcast.artworkURL } var episodes: [EpisodeCellViewModel] { return podcast.episodes.map(EpisodeCellViewModel.init) } } Tracking the Podcast To keep the track of podcast, we will create a view model having all the details of the podcast along with the subscription details obtained from the Core Data. var podcastLookupInfo: PodcastLookupInfo! private let subscriptionStore = SubscriptionStore.shared private var podcast: Podcast? { didSet { podcastViewModel = podcast.flatMap { PodcastViewModel(podcast: $0, isSubscribed: subscriptionStore.isSubscribed(to: $0.id)) } } information } private var podcastViewModel: PodcastViewModel? { didSet { headerViewController.podcast = podcastViewModel tableView.reloadData() } } To check if the podcast is subscribed, we will lookup in the Core Data for the subscription for a given podcast id. As we expect only one record while fetching the subscription request, we will set the .fetchLimit to 1. We will also set the format while fetching the record and try fetching the first record. func isSubscribed(to id: String) -> Bool { do { return try findSubscription(with: id) != nil } catch { return false } } func findSubscription(with podcastId: String) throws -> SubscriptionEntity? { let fetch: NSFetchRequest<SubscriptionEntity> = SubscriptionEntity.fetchRequest() fetch.fetchLimit = 1 fetch.predicate = NSPredicate(format: "podcast.id == %@", podcastId) return try mainContext.fetch(fetch).first } Next we will make the header oblivious to the Podcast model by using the reference of PodcastViewModel instead of Podcast. We will also set the selection of the subscribe button when a podcast is subscribed. private func updateUI(for podcast: PodcastViewModel) { subscribeButton.isEnabled = true subscribeButton.isSelected = podcast.isSubscribed let options: KingfisherOptionsInfo = [.transition(.fade(1.0))] podcast.artworkURL, placeholder: nil, options: options) titleLabel.text = podcast.title authorLabel.text = podcast.author genreLabel.text = podcast.genre descriptionLabel.text = podcast.description } Subscribing to a Podcast Now we are set to subscribe to a podcast. We will create a function returning the SubscriptionEntity, marked with @discardableResult attribute. We will then create a podcast using mainContext and assign the details of the podcast model to its entity object. Next, we will create a subscription by assigning date and podcast entity. @discardableResult func subscribe(to podcast: Podcast) throws -> SubscriptionEntity { let podcastEntity = PodcastEntity(context: mainContext) podcastEntity.id = podcast.id podcastEntity.title = podcast.title podcastEntity.podcastDescription = podcast.description podcastEntity.author = podcast.author podcastEntity.genre = podcast.primaryGenre podcastEntity.artworkURLString = podcast.artworkURL?.absoluteString podcastEntity.feedURLString = podcast.feedURL.absoluteString let subscription = SubscriptionEntity(context: mainContext) subscription.dateSubscribed = Date() subscription.podcast = podcastEntity try mainContext.save() return subscription } Next, we handle when the subscribe button is tapped.. @objc private func subscribeTapped(_ sender: SubscribeButton) { guard let podcast = podcast else { return } let isSubscribing = !sender.isSelected do { if isSubscribing { try subscriptionStore.subscribe(to: podcast) } else { try subscriptionStore.unsubscribe(from: podcast) } sender.isSelected.toggle() } catch { let action = isSubscribing ? "Subscribing to Podcast" : "Unsubscribing from Podcast" let alert = UIAlertController(title: "Error \(action)", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) } } Hook Into Tapped Event Finally, we will add the event handler for the subscribe button while it is been tapped. We will subscribe the podcast while the button is selected and unsubscribe while it is in the deselected state. override func viewDidLoad() { super.viewDidLoad() tableView.backgroundColor = Theme.Colors.gray5 tableView.separatorColor = Theme.Colors.gray4 headerViewController = children.compactMap { $0 as? PodcastDetailHeaderViewController }.first headerViewController.subscribeButton.addTarget(self, action: #selector(subscribeTapped(_:)), for: .touchUpInside) loadPodcast() } // MARK: - Event Handling @objc private func subscribeTapped(_ sender: SubscribeButton) { guard let podcast = podcast else { return } let isSubscribing = !sender.isSelected do { if isSubscribing { try subscriptionStore.subscribe(to: podcast) } else { try subscriptionStore.unsubscribe(from: podcast) } sender.isSelected.toggle() } catch { let action = isSubscribing ? "Subscribing to Podcast" : "Unsubscribing from Podcast" let alert = UIAlertController(title: "Error \(action)", message: error.localizedDescription, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) } } Now we will create a function to unsubscribe from the podcast. func unsubscribe(from podcast: Podcast) throws { if let sub = try findSubscription(with: podcast.id) { mainContext.delete(sub) try mainContext.save() } }