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
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 levelenumCompoundItemLevel{caseroot,section,item}protocolCompoundItemProtocol{// unique identifiervaridentifier:String{get}// level at which the element is locatedvarlevel:CompoundItemLevel{get}// list of child elementsvarchildren:[CompoundItemProtocol]{get}// list of items for usevaritems:[CompoundItemProtocol]{get}// add a child elementfuncadd(_model:CompoundItemProtocol...)// remove a child elementfuncremove(_model:CompoundItemProtocol)}// Ability to compare objects conforming to CompoundItemProtocolfunc==(lhs:CompoundItemProtocol,rhs:CompoundItemProtocol)->Bool{returnlhs.identifier==rhs.identifier}
The component defines a common interface for simple and composite tree elements.
Since all models must implement it, let's write a base class BaseCompoundItem on which we'll build the necessary models for our tree:
classBaseCompoundItem:CompoundItemProtocol{varchildren:[CompoundItemProtocol]=[]letlevel:CompoundItemLevelletidentifier:Stringvaritems:[CompoundItemProtocol]{returnchildren}// Initializing without parameters creates the root element of the structureinit(){self.identifier="root"self.level=.root}init(identifier:String,level:CompoundItemLevel){self.identifier=identifierself.level=level}funcadd(_model:CompoundItemProtocol...){self.children.append(contentsOf:model)}funcremove(_model:CompoundItemProtocol){ifletindex=self.children.index(where:{$0==model}){children.remove(at:index)}}}
The Container (or Composite) is a composite tree element.
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 fieldsprotocolSectionCompoundItemProtocol{varheader:String?{get}varfooter:String?{get}}classSectionCompoundItem:CompoundItemProtocol{privateletdecoratedComposite:CompoundItemProtocol// Main fields added via the decoratorvarheader:String?varfooter:String?varidentifier:String{returnself.decoratedComposite.identifier}// From the decorated object we recursively// request elements — returns only leavesvarchildren:[CompoundItemProtocol]{returnself.decoratedComposite.children}varitems:[CompoundItemProtocol]{returnself.decoratedComposite.children.flatMap{$0.items}}varlevel:CompoundItemLevel{returnself.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 levelself.decoratedComposite=BaseCompoundItem(identifier:identifier,level:.section)self.header=headerself.footer=footer}// A section can contain anything except other sections and root elementsfuncadd(_model:CompoundItemProtocol...){foriteminmodel{ifitem.level!=.section&&item.level!=.root{self.decoratedComposite.add(item)}else{print("You can`t add section in other section")}}}funcremove(_model:CompoundItemProtocol){self.decoratedComposite.remove(model)}}
The section model is also a container since it can contain child elements.
And the model for a cell:
// A read-only protocol with only the necessary fieldsprotocolCellCompoundItemProtocol{vartitle:String?{get}varsubtitle:String?{get}varcellIdentifier:String{get}}classCellCompoundItem:CompoundItemProtocol,CellCompoundItemProtocol{privateletdecoratedComposite:CompoundItemProtocolvartitle:String?varsubtitle:String?letcellIdentifier:Stringvaridentifier:String{returnself.decoratedComposite.identifier}// This is a tree leaf, so return selfvartems:[CompoundItemProtocol]{return[self]}// Leaves cannot have childrenvarchildren:[CompoundItemProtocol]{return[]}varlevel:CompoundItemLevel{returnself.decoratedComposite.level}init(identifier:String,cellIdentifier:String,title:String?,subtitle:String?){self.decoratedComposite=BaseCompoundItem(identifier:identifier,level:.item)self.title=titleself.subtitle=subtitleself.cellIdentifier=cellIdentifier}funcadd(_model:CompoundItemProtocol...){print("Cannot add child elements — this element is a leaf of the structure")}funcremove(_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 containerself.datasource=BaseCompoundItem()// Create section modelsletsection1=SectionCompoundItem(identifier:"Section1",header:"MARVEL Universe",footer:"Marvel Comics — an American comic book publisher, a division of Marvel Entertainment")letsection2=SectionCompoundItem(identifier:"Section2",header:"DC Universe",footer:"DC Comics — one of the largest and most popular comic book publishers, alongside Marvel Comics")// Create cell modelsletcap=CellCompoundItem(identifier:"cell2",cellIdentifier:"CellID",title:"Captain America",subtitle:"Steven Rogers")letbatman=CellCompoundItem(identifier:"cell5",cellIdentifier:"CellID",title:"Batman",subtitle:"Bruce Wayne")// Add cell models to sections, and sections to the root containersection1.add(cap)section2.add(batman)datasource.add(section1,section2)
Now call tableView.reloadData() and you’re done. See the full example here
Conclusion
Useful additions you can make:
Cell visibility filters. Add an isVisible parameter — it controls whether models are returned.