
This video is only available to subscribers. Start a subscription today to get access to this and 470 other videos.
Setting up Core Data to Save Subscriptions
This episode is part of a series: Making a Podcast App From Scratch.
Episode Links
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()
}
}