Chapters

Hide chapters

Design Patterns by Tutorials

Third Edition · iOS 13 · Swift 5 · Xcode 11

20. Composite Pattern
Written by Jay Strawn

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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:

  1. The component protocol ensures all constructs in the tree can be treated the same way.
  2. A leaf is a component of the tree that does not have child elements.
  3. 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.

import Foundation

protocol File {
  var name: String { get set }
  func open()
}
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")
  }
}
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")
  }
}
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()

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.

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
  }
}
final class ToDoItem: ToDo {
  var name: String
  var isComplete: Bool
  var subtasks: [ToDo]
  
  init(name: String) {
    self.name = name
    isComplete = false
    subtasks = []
  }
}
var toDos: [ToDo] = []
var completedToDos: [ToDo] = []
let currentToDo = toDos[indexPath.row]
var currentToDo = toDos[indexPath.row]
var subtasks: [ToDoItem] = []
var subtasks: [ToDo] = []
let currentToDo = subtasks[indexPath.row]
var currentToDo = subtasks[indexPath.row]
if currentToDo is ToDoItemWithCheckList {
  cell.subtasks = currentToDo.subtasks
}
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)
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)
}
controller.addAction(
  UIAlertAction(title: "Task with Checklist", style: .default) { 
  [weak self] _ in

  self?.createTaskWithChecklist()
})

Key points

You learned about the composite pattern in this chapter. Here are its key points:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now