Before we continue populating the user interface of the feed view controller, we need to resolve an issue we introduced in the previous episode. The feed view controller asks its view model for an Episode object every time it needs to configure an episode collection view cell. The project adopts the Model-View-ViewModel pattern, which means that the view controller should not have direct access to model objects. In this episode, I show you a simple solution to hide the Episode object from the feed view controller.
Defining a Protocol
The idea is simple. We hide the Episode object behind a protocol. Let me show you how that works. We add a Swift file with name EpisodePresentable.swift to the Protocols group and define a protocol with name EpisodePresentable.
import Foundation
protocol EpisodePresentable {
}
We keep the protocol definition simple for the time being. We define several properties, title of type String, collection of type String?, thumbnailUrl of type URL, andpublishedAtof typeDate`.
import Foundation
protocol EpisodePresentable {
// MARK: - Properties
var title: String { get }
var collection: String? { get }
// MARK: -
var thumbnailUrl: URL { get }
// MARK: -
var publishedAt: Date { get }
}
The next step is conforming the Episode struct to the EpisodePresentable protocol. We could create a separate file for this purpose, but I prefer to create the extension in EpisodePresentable.swift. That's a personal choice. Because the Episode struct implicitly conforms to the EpisodePresentable protocol, we can leave the implementation of the extension empty.
import Foundation
protocol EpisodePresentable {
// MARK: - Properties
var title: String { get }
var collection: String? { get }
// MARK: -
var thumbnailUrl: URL { get }
// MARK: -
var publishedAt: Date { get }
}
extension Episode: EpisodePresentable {}
Updating the Feed View Model
With the EpisodePresentable protocol in place, it's time to update the FeedViewModel class. The feed view controller should not have direct access to Episode objects and that means declaring the episode(at:) method privately.
private func episode(at index: Int) -> Episode {
return episodes[index]
}
The feed view controller should have the ability to ask its view model for an object that conforms to the EpisodePresentable protocol. An object that conforms to the EpisodePresentable protocol exposes the data the feed view controller needs to configure the cells of its collection view.
We define a method with name presentable(for:). It accepts an integer as its only argument and it returns an object that conforms to the EpisodePresentable protocol. The implementation is surprisingly simple. We invoke the episode(at:) method and return the result. You could say that the presentable(for:) method acts as an alias of the episode(at:) method.
func presentable(for index: Int) -> EpisodePresentable {
return episode(at: index)
}
It is important that you understand what we have accomplished because the change is subtle. The feed view model still returns an Episode object to the feed view controller. This is odd. Right?
The solution becomes clear when we inspect the public API of the FeedViewModel class. If an object, such as the feed view controller, asks the feed view model for an object to configure an episode collection view cell, it receives an object that conforms to the EpisodePresentable protocol. The feed view controller has no idea that the concrete type of the object is Episode and it doesn't care. It only knows and cares that the object conforms to the EpisodePresentable protocol.
Updating the Feed View Controller
Let's update the feed view controller. Revisit the collectionView(_:cellForItemAt:) method of the UICollectionViewDataSource protocol. We start by updating the guard statement. The feed view controller no longer asks its view model for an Episode object. It invokes the presentable(for:) method on the FeedViewModel instance and receives an object conforming to the EpisodePresentable protocol.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Fetch Presentable
guard let presentable = viewModel?.presentable(for: indexPath.item) else {
fatalError("No Presentable Available")
}
...
}
The next step is using the episode presentable to configure the episode collection view cell. Views should be kept as dumb as possible, but I tend to deviate from that rule in certain scenarios. This is such a scenario.
The implementation of the collectionView(_:cellForItemAt:) method is less cluttered if the episode collection view cell has the ability to configure itself if the feed view controller gives it the data it needs.
Table and collection view cells are interesting views. The code you write is often more elegant if you treat a table or collection view cell as a view controller because that is what table and collection view cells sometimes are. They control a view and its subviews. The view they control is the content view of the table or collection view cell.
Open EpisodeCollectionViewCell.swift and define a method, configure(with:), that accepts an object conforming to the EpisodePresentable protocol. In the body of the configure(with:) method, the episode collection view cell uses the episode presentable to configure the title label.
// MARK: - Public API
func configure(with presentable: EpisodePresentable) {
// Configure Title Label
titleLabel.text = presentable.title
}
Because the EpisodeCollectionViewCell class configures its user interface elements, we can declare the outlets privately. No other objects should have access to the label and the image view.
import UIKit
class EpisodeCollectionViewCell: UICollectionViewCell {
// MARK: - Properties
@IBOutlet private var imageView: UIImageView!
// MARK: -
@IBOutlet private var titleLabel: UILabel!
...
}
The last piece of the puzzle is passing the episode presentable to the configure(with:) method of the EpisodeCollectionViewCell instance in the collectionView(_:cellForItemAt:) method.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Fetch Presentable
guard let presentable = viewModel?.presentable(for: indexPath.item) else {
fatalError("No Presentable Available")
}
// Dequeue Episode Collection View Cell
let cell: EpisodeCollectionViewCell = collectionView.dequeueReusableCell(for: indexPath)
// Configure Cell
cell.configure(with: presentable)
return cell
}
What have we accomplished in this episode? The feed view controller no longer has direct access to Episode objects. We also improved the implementation of the collectionView(_:cellForItemAt:) method. Remember that we plan to reuse the EpisodeCollectionViewCell class in several places. By putting the EpisodeCollectionViewCell class in charge of configuring itself, we avoid or at least reduce code duplication.
What's Next?
We made a subtle but important improvement in this episode. Because the project adopts the MVVM pattern, we need to play by the rules of the pattern. The solution we implemented is simple and elegant.
In the next episode, we populate the cells of the collection view of the feed view controller with images. This is more challenging than it seems because the Cocoacasts API returns an SVG image, which isn't supported by the UIKit framework. We use a third party service to resolve this issue.