Demo
Head back to Xcode to see how this works. First, create an extension on MediaCollection:
extension MediaCollection {
}
Then, define a function to make it easy to add items to that collection:
mutating func add(_ item: Item) {
self.items.append(item)
}
Since the compiler knows that MediaCollection has an associated type of Item, you can use that in the extension.
Next, use the new helper function and change the code where you add movies to the collection:
movieCollection.add(bourneIdentity)
movieCollection.add(oppenheimer)
Next, add a default implementation for getDescription():
func getDescription() -> String {
"Collection contains \(items.count) items"
}
Run the playground. You’ll see that it still uses the implementation from MovieCollection, because that takes precedence over the default implementation.
Now, remove the implementation of getDescription() from MovieCollection and run the playground. The code still compiles, and MovieCollection still conforms to MediaCollection, because getDescription() is implemented in the protocol extension. This time, it prints the message from the default implementation.
Next, you’ll see how to use generics to improve the code further. At the bottom of the playground, create a new type called MediaShelf that can represent any type of media:
struct MediaShelf: MediaCollection {
}
Then, add a generic type for the items to satisfy the protocol:
var items: [T] = []
Remember, this is all you need to conform to MediaCollection.
Finally, add the generic type to MediaShelf. By conforming it to MediaItem, you’ll satisfy the compiler’s requirements, enabling it to understand how to properly interface the protocol with the given type:
struct MediaShelf<T: MediaItem>: MediaCollection
Now, you can create some media items and shelves, then link them all together:
let catan = BoardGame(title: "Catan", price: 40)
let arkhamKnight = VideoGame(title: "Batman: Arkham Knight", price: 49.99, console: .xbox)
let tearsOfTheKingdom = VideoGame(title: "The Legend of Zelda: Tears of the Kingdom", price: 59.99, console: .switch)
let noTimeToDie = Movie(title: "No Time To Die", price: 19.99, duration: 163)
var boardGameShelf = MediaShelf<BoardGame>()
var movieShelf = MediaShelf<Movie>()
var videoGameShelf = MediaShelf<VideoGame>()
boardGameShelf.add(catan)
movieShelf.add(noTimeToDie)
videoGameShelf.add(arkhamKnight)
videoGameShelf.add(tearsOfTheKingdom)
print(boardGameShelf.getDescription())
print(movieShelf.getDescription())
print(videoGameShelf.getDescription())
Run the playground. You’ll see all the descriptions for the collections.
Finally, write an extension on MediaShelf to improve the description for video games:
extension MediaShelf where T == VideoGame {
func getDescription() -> String {
let xboxCount = items.filter({ $0.console == .xbox }).count
let playstationCount = items.filter({ $0.console == .playstation }).count
let switchCount = items.filter({ $0.console == .switch }).count
return "This collection contains \(xboxCount) Xbox games, \(playstationCount) Playstation games and \(switchCount) Switch games"
}
}
This extension is constrained to instances of MediaShelf where the items are exclusively video games. This is a really powerful feature to provide complex customization when using generics.
Run the playground. You’ll see the new description for the video game shelf.
You can also restrict the generic type to a protocol:
extension MediaShelf where T: Codable {
func printItems() throws {
let jsonEncoder = JSONEncoder()
let data = try jsonEncoder.encode(items)
if let string = String(data: data, encoding: .utf8) {
print(string)
}
}
}
This adds a new function to any MediaShelf where the items are Codable. Conform BoardGame to Codable:
struct BoardGame: MediaItem, Codable
And call the new function:
try boardGameShelf.printItems()
This prints the items on the shelf as a JSON string.