20.
Composite Pattern
Written by Jay Strawn
The composite pattern is a structural pattern that groups a set of objects into a tree structure so they may be manipulated as though they were one object. It uses three types:
- The component protocol ensures all constructs in the tree can be treated the same way.
- A leaf is a component of the tree that does not have child elements.
- A composite is a container that can hold leaf objects and composites.
Both composites and leaf nodes derive from the component protocol. You can even have several different leaf classes held in a composite object.
For example, an Array is a composite. The component is the Array itself. The composite is a private container used by Array to contain leaf objects. Each leaf is a concrete type such as Int, String or whatever you add to the Array.
When should you use it?
If your app’s class hierarchy forms a branching pattern, trying to create two types of classes for branches and nodes can make it difficult for those classes to communicate.
You can solve this problem with the composite pattern by treating branches and nodes the same by making them conform to a protocol. This adds a layer of abstraction to your models and ultimately reduces their complexity.
Playground example
Open AdvancedDesignPatterns.xcworkspace in the Starter directory, and then open the Composite page.
For this playground example, you’ll make an app that stores different elements in a tree pattern.
A file hierarchy is an everyday example of the composite pattern. Think about files and folders. All .mp3 and .jpeg files, as well as folders, share a lot of functions: “open”, “move to trash,” “get info,” “rename,” etc. You can move and store groups of different files, even if they aren’t all the same type, because they all conform to a component protocol.
To make your own file hierarchy in the playground, add the following after Code Example:
import Foundation
protocol File {
var name: String { get set }
func open()
}
You’ve just created a component protocol, which all the leaf objects and composites will conform to. Next, you’re going to add a couple of leaf objects. Add the following to the end of the playground:
final class eBook: File {
var name: String
var author: String
init(name: String, author: String) {
self.name = name
self.author = author
}
func open() {
print("Opening \(name) by \(author) in iBooks...\n")
}
}
final class Music: File {
var name: String
var artist: String
init(name: String, artist: String) {
self.name = name
self.artist = artist
}
func open() {
print("Playing \(name) by \(artist) in iTunes...\n")
}
}
You’ve added two leaf objects that conform to the component protocol. They all have a name property and an open function, but each open() varies based on the object’s class.
Next, add the following code to the end of the playground:
final class Folder: File {
var name: String
lazy var files: [File] = []
init(name: String) {
self.name = name
}
func addFile(file: File) {
self.files.append(file)
}
func open() {
print("Displaying the following files in \(name)...")
for file in files {
print(file.name)
}
print("\n")
}
}
Your Folder object is a composite, and it has an array that can hold any object that conforms to the File protocol. This means that, not only can a Folder hold Music and eBook objects, it can also hold other Folder objects.
Feel free to play around with creating objects and placing them in folders within the playground. Here’s one example showcasing a few leaf objects and composites:
let psychoKiller = Music(name: "Psycho Killer",
artist: "The Talking Heads")
let rebelRebel = Music(name: "Rebel Rebel",
artist: "David Bowie")
let blisterInTheSun = Music(name: "Blister in the Sun",
artist: "Violent Femmes")
let justKids = eBook(name: "Just Kids",
author: "Patti Smith")
let documents = Folder(name: "Documents")
let musicFolder = Folder(name: "Great 70s Music")
documents.addFile(file: musicFolder)
documents.addFile(file: justKids)
musicFolder.addFile(file: psychoKiller)
musicFolder.addFile(file: rebelRebel)
blisterInTheSun.open()
justKids.open()
documents.open()
musicFolder.open()
You’re able to treat all of these objects uniformly and call the same functions on them. But, to quote the Talking Heads song mentioned above: “Qu’est-ce que c’est? (What does this mean?)”
Using composite patterns becomes meaningful when you’re able to treat different objects the same way, and reusing objects and writing unit tests becomes much less complicated.
Imagine trying to create a container for your files without using a component protocol! Storing different types of objects would get complicated very quickly.
What should you be careful about?
Make sure your app has a branching structure before using the composite pattern. If you see that your objects have a lot of nearly identical code, conforming them to a protocol is a great idea, but not all situations involving protocols will require a composite object.
Tutorial project
Throughout this section, you’ll add functionality to an app called Defeat Your ToDo List.
In the Projects ▸ Starter directory, open DefeatYourToDoList\DefeatYourToDoList.xcodeproj in Xcode. This app allows the user to add items to a to-do list.
As the user checks items off, a warrior at the top of the screen moves closer to treasure at the end of a dungeon. The warrior reaches the end when the user completes 100% of the tasks.
In this project, you’re going to add a feature in which a user can create a task that holds smaller tasks within, like a checklist.
First, open Models.swift and add the following below import Foundation:
protocol ToDo {
var name: String { get set }
var isComplete: Bool { get set }
var subtasks: [ToDo] { get set }
}
final class ToDoItemWithCheckList: ToDo {
var name: String
var isComplete: Bool
var subtasks: [ToDo]
init(name: String, subtasks: [ToDo]) {
self.name = name
isComplete = false
self.subtasks = subtasks
}
}
Here, you’ve added a component protocol, called ToDo, to which all of your to-do objects should conform. You’ve also added a composite object called ToDoItemWithCheckList, which stores your checklist items in an array called subtasks.
Now, in order to actually use the composite pattern, you need to make your default to-do conform to the component protocol. Still in Models.swift, replace ToDoItem with the following code:
final class ToDoItem: ToDo {
var name: String
var isComplete: Bool
var subtasks: [ToDo]
init(name: String) {
self.name = name
isComplete = false
subtasks = []
}
}
You’ll notice that, in order to have your default ToDoItem conform to the ToDo protocol, you have to give it a subtasks property. While initializing subtasks as an empty array may seem like an unnecessary added complexity, you’ll see in the next steps that having both classes include all possible properties makes it easier to reuse the custom ToDoCell for the collection view in your view controller.
Next, open ViewController.swift. You want to start refactoring at the top, underneath the IBOutlet connections. Each task is stored in an array called toDos and, when completed, they are added to completedToDos. There are two arrays so that you know the percentage of tasks completed, which will move the warrior along the path.
First, you want both arrays to accept items that conform to the component protocol instead of simply ToDoItem. Replace the two properties with the following:
var toDos: [ToDo] = []
var completedToDos: [ToDo] = []
You should get a compiler error in collectionView(_:didSelectItemAt:). To fix this error, inside collectionView(_:didSelectItemAt:), replace:
let currentToDo = toDos[indexPath.row]
With the following:
var currentToDo = toDos[indexPath.row]
You have to do this because Swift can’t figure out whether the protocol, ToDo, is a struct or a class.
If it were a struct, then currentToDo would have to be declared var to be able to mutate it. Of course, you know it’s always actually a class though.
Next, open ToDoCell.swift and replace:
var subtasks: [ToDoItem] = []
With the following:
var subtasks: [ToDo] = []
Similar to what you did in ViewController.swift, you’ll need to scroll to collectionView(_:didSelectItemAt:) and replace:
let currentToDo = subtasks[indexPath.row]
With the following:
var currentToDo = subtasks[indexPath.row]
Next, open ViewController.swift. Now, it’s time to get your collection view cells to display both ToDoItem and ToDoItemWithCheckList.
Start by navigating to collectionView(_:cellForItemAt:) in the UICollectionViewDataSource extension.
Add the following just above return cell:
if currentToDo is ToDoItemWithCheckList {
cell.subtasks = currentToDo.subtasks
}
This if statement populates the subtasks in ToDoCell. The other collection view on the custom ToDoCell is already set up for you, so no changes need to be made there.
Next, for the collection view located in the view controller, you want to be able to change the cell’s height based on how many subtasks are on the checklist of your to-do item.
Scroll down to collectionView(_:layout:sizeForItemAt:) and replace its contents with the following:
let width = collectionView.frame.width
let currentToDo = toDos[indexPath.row]
let heightVariance = 60 * (currentToDo.subtasks.count)
let addedHeight = CGFloat(heightVariance)
let height = collectionView.frame.height * 0.15 + addedHeight
return CGSize(width: width, height: height)
Now, each cell’s height will increase by 60 for each subtask in the composite to-do item.
Now, it’s time to add the ability for the user to create a ToDoItemWithCheckList! Add the following method to the end of the MARK: - Internal extension:
func createTaskWithChecklist() {
let controller = UIAlertController(
title: "Task Name",
message: "",
preferredStyle: .alert)
controller.addTextField { textField in
textField.placeholder = "Enter Task Title"
}
for _ in 1...4 {
controller.addTextField { textField in
textField.placeholder = "Add Subtask"
}
}
let saveAction = UIAlertAction(title: "Save",
style: .default) {
[weak self] alert in
let titleTextField = controller.textFields![0]
let firstTextField = controller.textFields![1]
let secondTextField = controller.textFields![2]
let thirdTextField = controller.textFields![3]
let fourthTextField = controller.textFields![4]
let textFields = [firstTextField,
secondTextField,
thirdTextField,
fourthTextField]
var subtasks: [ToDo] = []
for textField in textFields where textField.text != "" {
subtasks.append(ToDoItem(name: textField.text!))
}
let currentToDo = ToDoItemWithCheckList(
name: titleTextField.text!,
subtasks: subtasks)
self?.toDos.append(currentToDo)
self?.toDoListCollectionView.reloadData()
self?.setWarriorPosition()
}
let cancelAction = UIAlertAction(title: "Cancel",
style: .default)
controller.addAction(saveAction)
controller.addAction(cancelAction)
present(controller, animated: true)
}
This function adds a ToDoItemWithCheckList to the todos array, reloads the collection view and resets the warrior’s position. Now, all that’s left to do is to add the ability to call this function from the UIAlertController. Add the following code inside addToDo(_:) above present(alertController, animated: true):
controller.addAction(
UIAlertAction(title: "Task with Checklist", style: .default) {
[weak self] _ in
self?.createTaskWithChecklist()
})
All set! Now you can add as many to-do items as you like. Build and run the app. Try out the new functionality, and go get that treasure!
Key points
You learned about the composite pattern in this chapter. Here are its key points:
-
The composite pattern is a structural pattern that groups a set of objects into a tree so that they may be manipulated as though they were one object.
-
If your app’s class hierarchy forms a branching pattern, you can treat branches and nodes as almost the same objects by conforming them to a component protocol. The protocol adds a layer of abstraction to your models, which reduces their complexity.
-
This is a great pattern to help simplify apps that have multiple classes with similar features. With it, you can reuse code more often and reduce complexity in your classes.
-
A file hierarchy is an everyday example of the composite pattern. All
.mp3and.jpegfiles, as well as folders, share a lot of functions such as “open” and “move to trash.” You can move and store groups of different files, even if they aren’t all the same type, as they all conform to a component protocol.
With your Defeat Your ToDo List app now using a composite pattern, it’s really convenient that you can reuse the same custom cell on both ToDoItem and ToDoItemWithCheckList. Also, since a ToDoItemWithCheckList can hold another ToDoItemWithCheckList, you could actually write this app to have an infinite number of checklists within checklists! (We wouldn’t recommend that on such a tiny screen, though!)