Composite
Published: 3 November 2017
Tagged:
Pattern

Table representations such as UITableView and UICollectionView are common in iOS apps. Depending on the task, I used either an array or a dictionary as the data structure. This approach had several limitations and wasn’t flexible. I found the Composite pattern to be much more convenient. Project with example on GitHub :octocat:


Composite is a structural design pattern that lets you compose objects into tree structures and then work with those structures as if they were individual objects.



Theory

What will our tree look like? A table view consists of a list of sections. Sections, in turn, contain cell models (same for collections). We need elements of two types: those that can contain child elements (containers) and terminal elements (leaves) that cannot have children.


Container (Composite) — a composite tree element. It contains child elements — Leaves or other Containers — but doesn’t know which specific ones, because it works with them only through the common Component interface (CompoundItemProtocol). This class’s methods delegate the main work to its child components, while possibly adding something of its own to the result. Thus, the Composite pattern allows us to work with object trees of any complexity, without paying attention to the specific models forming the tree.



Implementation

  1. Write a common interface for tree elements CompoundItemProtocol — all elements in our structure must implement this protocol. Also create an enum with available nesting levels CompoundItemLevel.
    // nesting level
    enum CompoundItemLevel {
      case root, section, item
    }
    
    protocol CompoundItemProtocol {
      // unique identifier
      var identifier: String {get}
      // level at which the element is located
      var level: CompoundItemLevel {get}
      // list of child elements
      var children: [CompoundItemProtocol] {get} 
      // list of items for use
      var items: [CompoundItemProtocol] {get}
      // add a child element
      func add(_ model: CompoundItemProtocol...)
      // remove a child element
      func remove(_ model: CompoundItemProtocol)
    }
    
    // Ability to compare objects conforming to CompoundItemProtocol
    func == (lhs: CompoundItemProtocol, rhs: CompoundItemProtocol) -> Bool {
      return lhs.identifier == rhs.identifier
    }
    The component defines a common interface for simple and composite tree elements.
  2. Since all models must implement it, let's write a base class BaseCompoundItem on which we'll build the necessary models for our tree:
    class BaseCompoundItem: CompoundItemProtocol {
      var children: [CompoundItemProtocol] = []
      let level: CompoundItemLevel
      let identifier: String
      
      var items: [CompoundItemProtocol] {
        return children
      }
      
      // Initializing without parameters creates the root element of the structure
      init() {
        self.identifier = "root"
        self.level = .root
      }
      
      init(identifier: String, level: CompoundItemLevel) {
        self.identifier = identifier
        self.level = level
      }
      
      func add(_ model: CompoundItemProtocol...) {
        self.children.append(contentsOf: model)
      }
      
      func remove(_ model: CompoundItemProtocol) {
        if let index = self.children.index(where: { $0 == model }) {
          children.remove(at: index)
        }
      }
    }
    The Container (or Composite) is a composite tree element.
  3. The base implementation is ready. We can now add concrete models: sections and cells. To create models for tree elements, we'll use the Decorator pattern to extend our base BaseCompoundItem implementation. This structural pattern allows dynamically adding new functionality to objects using so-called wrappers. Let's write the model for a section:
    // A read-only protocol with only the necessary fields
    protocol SectionCompoundItemProtocol {
      var header: String? {get}
      var footer: String? {get}
    }
    
    class SectionCompoundItem: CompoundItemProtocol {
      private let decoratedComposite: CompoundItemProtocol
      
      // Main fields added via the decorator
      var header: String?
      var footer: String?
      
      var identifier: String {
        return self.decoratedComposite.identifier
      }
      
      // From the decorated object we recursively
      // request elements — returns only leaves
      var children: [CompoundItemProtocol] {
        return self.decoratedComposite.children
      }
      
      var items: [CompoundItemProtocol] {
        return self.decoratedComposite.children.flatMap {$0.items}
      }
      
      var level: CompoundItemLevel {
        return self.decoratedComposite.level
      }
      
      init(identifier: String, header: String?, footer: String?) {
        // For simplicity, we don't pass the decorated object,
        // but simply create it with the required nesting level
        self.decoratedComposite = BaseCompoundItem(identifier: identifier, level: .section)
        self.header = header
        self.footer = footer
      }
      
      // A section can contain anything except other sections and root elements
      func add(_ model: CompoundItemProtocol...) {
        for item in model {
          if item.level != .section && item.level != .root {
            self.decoratedComposite.add(item)
          } else {
            print("You can`t add section in other section")
          }
        }
      }
      
      func remove(_ model: CompoundItemProtocol) {
        self.decoratedComposite.remove(model)
      }
    }
    The section model is also a container since it can contain child elements.
  4. And the model for a cell:
    // A read-only protocol with only the necessary fields
    protocol CellCompoundItemProtocol {
      var title: String? {get}
      var subtitle: String? {get}
      var cellIdentifier: String {get}
    }
    
    class CellCompoundItem: CompoundItemProtocol, CellCompoundItemProtocol {
      private let decoratedComposite: CompoundItemProtocol
      var title: String?
      var subtitle: String?
      let cellIdentifier: String
      
      var identifier: String {
        return self.decoratedComposite.identifier
      }
      
      // This is a tree leaf, so return self
      var tems: [CompoundItemProtocol] {
        return [self]
      }
      
      // Leaves cannot have children
      var children: [CompoundItemProtocol] {
        return []
      }
      
      var level: CompoundItemLevel {
        return self.decoratedComposite.level
      }
      
      init(identifier: String, cellIdentifier: String, title: String?, subtitle: String?) {
        self.decoratedComposite = BaseCompoundItem(identifier: identifier, level: .item)
        self.title = title
        self.subtitle = subtitle
        self.cellIdentifier = cellIdentifier
      }
      
      func add(_ model: CompoundItemProtocol...) {
        print("Cannot add child elements — this element is a leaf of the structure")
      }
      
      func remove(_ model: CompoundItemProtocol) {
        print("Leaves have no children, nothing to remove")
      }
    }
    The cell model is a leaf — a simple tree element with no branches.

Usage

The code is written, let’s try out our new data structure:

// Create the root container
self.datasource = BaseCompoundItem()

// Create section models
let section1 = SectionCompoundItem(identifier: "Section1", header: "MARVEL Universe", footer: "Marvel Comics — an American comic book publisher, a division of Marvel Entertainment")
let section2 = SectionCompoundItem(identifier: "Section2", header: "DC Universe", footer: "DC Comics — one of the largest and most popular comic book publishers, alongside Marvel Comics")

// Create cell models
let cap = CellCompoundItem(identifier: "cell2", cellIdentifier: "CellID", title: "Captain America", subtitle: "Steven Rogers")
let batman = CellCompoundItem(identifier: "cell5", cellIdentifier: "CellID", title: "Batman", subtitle: "Bruce Wayne")
    
// Add cell models to sections, and sections to the root container
section1.add(cap)
section2.add(batman)
datasource.add(section1, section2)

Now call tableView.reloadData() and you’re done. See the full example here :cat:



Conclusion

Useful additions you can make:

  • Cell visibility filters. Add an isVisible parameter — it controls whether models are returned.
// CellCompoundItem
var isVisible = false
var items: [CompoundItemProtocol] {
    return self.isVisible ? [self] : []
}
  • Validate all table fields. Add a new method func isValid() -> Bool to CompoudItemProtocol. Implement it in all cell models of our tree.
  • Track data changes in the table when building complex input forms (similar to point 2).

Questions? Write here – @alobanov