DynamicStackView: A love/hate compromise

I have a love/hate relationship with UITableView. I also have a love/hate relationship with UIStackView. It’s like they’re perfect for the exact purpose they were built for, but once you start looking at custom solutions they’ll be quick to slap you in the face.

So, a while ago I was working on a chatbot of sorts. It had lots of nice animations and moving parts, conversation flow from the bottom, non-linear history flow, etc. You know, chatty stuff. While I knew from the start that this would require some custom solutions to work in tandem with e.g. a UITableView, the actual dealbreaker was the fact that UITableView deallocates cells outside its visible view portion. Adding, animating and otherwise manipulating data while keeping track of what was available - and not - at certain points in time seemed… “impractical”. Also, the chat history was divided into chunks with question + answer, meaning I really only wanted a limited set of rows for each “Q&A” anyway. Thus I tried out a conceptual mix of both UITableView and UIStackView, dubbing it DynamicStackView.

While the concept itself is nothing new - adding views to a container programmatically - the application of UITableView mechanics to a UIStackView is perhaps a little less conventional. It gives us a simpler version of a UITableView while adding an otherwise nonexistent model-cell-table relationship to a UIStackView.

A quick rundown

Basically there are three parts to DynamicStackView:

  1. DynamicStackViewModel
    • Protocol for associating generic models with DynamicStackViewCells.
  2. DynamicStackViewCell
    • View for subclassing to get access to associated models.
  3. DynamicStackView
    • Custom UIStackView containing all DynamicStackViewCells.

A simplified version of the cell creation method in our custom UIStackView (ie. DynamicStackView) shows us the core idea of the model-cell-table relationship:

private func createCell(from model: DynamicStackViewModel) -> DynamicStackViewCell {
    let cell = model.cellType.init()
    cell.setup(model: model)
    return cell
}

Subclassing a DynamicStackViewCell gives us access to the setup method above, thereby also giving us access to the associated model. Then, by exposing accessor functions like…

public func append(model: DynamicStackViewModel) {
    let cell = createCell(from: model)
    addArrangedSubview(cell)
}

… it’s really convenient for the developer to simply add models to the DynamicStackView and have those delivered to the associated cell. The actual connection is handled by the DynamicStackViewModel protocol:

public protocol DynamicStackViewModel {
    var cellType: DynamicStackViewCell.Type { get }
}

This way, a nice and tidy extension to any generic model connects it to a subclassed DynamicStackViewCell, leaving just a tiny footprint on the code outside the framework.

An actual implementation

DynamicStackViewModel - Protocol

All models to be used with DynamicStackView has to adhere to this protocol. As stated earlier, you’ll be associating them with a DynamicStackViewCell to automatically generate views in the DynamicStackView for you:

extension Content: DynamicStackViewModel {
    var cellType: DynamicStackViewCell.Type {
        return ContentCell.self
    }
}

DynamicStackViewCell - Superclass

By subclassing DynamicStackViewCell you can (must) override its setup method to get access to the associated DynamicStackViewModel:

override func setup(model: DynamicStackViewModel) {
    if let model = model as? Content {
        label.text = model.text
    }
}

DynamicStackView - Container

Simply add a new IBOutlet:

@IBOutlet weak var dynamicStackView: DynamicStackView!

Now add your DynamicStackViewModel compatible models to it:

let content = Content(text: "My content")
dynamicStackView.append(model: content)

Content added

Or as an array:

let contentArray = [
    Content(text: "My content"),
    Content(text: "Some more content")
]
dynamicStackView.append(models: contentArray)

Content added as array

If you want to manually override the cell type, simply specify it when adding models:

let overriddenContent = Content(text: "Overridden content")
dynamicStackView.append(model: overriddenContent, cellType: OtherContentCell.self)

Content added with another cell type

Note: When overriding the cell type, make sure that your subclassed DynamicStackViewCell can handle the new DynamicStackViewModel:

override func setup(model: DynamicStackViewModel) {
    if let model = model as? Content {
        label.text = model.text
    } else if let model = model as? OtherContent {
        label.text = model.text
    }
}

Finally, you can get access to the tapped cell through a callback block:

dynamicStackView.didTapCell = { cell in
    print(cell)
}

Wrap-up

With a pretty modest line count we now have a quick and lightweight framework handling lists of views in an automated fashion. Want rows at the bottom instead? Simply adjust your constraints in Interface Builder. Want scroll? Wrap everything in a scroll view.

Granted, DynamicStackView won’t (and shouldn’t) replace a UITableView in most projects, but for smaller lists - or those edge case chatbots - I think it’s a decent alternative.

You can find the complete framework project at github/varvet/DynamicStackView, and a simplified example project to play with at github/varvet/DynamicStackView/Example.