I am facing a problem with creation of dynamic view in Swift. However, the problem is not directly related to Swift itself, it is rather a Object-Oriented programming problem.
The problem is that I need to be able to add additional view elements to a view dynamically. And I am not sure if I'm doing it correctly. My solutions seems as overkill to me.
To solve the problem I thought Decorator pattern would be a good candidate. Additionally to have more control of the flow, I have introduced Template Method pattern.
I have a number of classes that define default look and feel on certain view controls like Labels, TextFields and Buttons. Here below you can see an approximate idea of how it is.
Here is my code:
class ViewElement{
// this class inherits from default UIKit elemnts and provides default UI view
}
// default cell is the cell that implements default elements layout and margings, etc
class DefaultCell: UITableViewCell {
let mainStack: UIViewStack
func addElement(ViewElement)
}
class BlueCell: DefaultCell {
let textField1: TextField
let label : Label
let button: Button
init(){
textField = TextField()
label = Label()
button = Button()
addElement(textField)
addElement(label)
addElement(button)
}
}
Here is the tableViewDataSource implementation
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
return cell
}
}
As you can see, the greater the number of conditions, the bigger my switch statement.
Additional problem arises from the fact that I will need to access the additional elements that I assign in the condition, like callbacks, tap events etc. Also the fact that those elements in conditional are added via addElement method, means that those elements will be added at the bottom of the mainStack.
In order to have control over the way elements are added to the stack I decided to go with the following solution: Template Method pattern
protocol OrderableElements {
func top()
func middle()
func bottom()
}
extension OrderableElements {
func render() {
top()
middle()
bottom()
}
}
Now the BlueCell implements the protocol and looks like this
class BlueCell: DefaultCell, OrderableElements {
init(){
textField = TextField()
label = Label()
button = Button()
}
func top() {
addElement(textField)
}
func middle() {
addElement(label)
}
func bottom(){
addElement(button)
}
}
The tabledatasource class will then look as follows:
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
...
**cell.render()**
return cell
}
}
Now because I need to add the new view elements in certain location or better said, at certain moments in during the scope of BlueCell, I introduced Decorators for the cell, like this:
class CellDecorator: OrderableElements {
var cell: BlueCell
init(cell: BlueCell){
self.cell = cell
}
func top() {
self.cell.top()
}
func middle(){
self.cell.middle()
}
func bottom(){
self.cell.bottom()
}
func getCell() {
return self.cell
}
}
Here is the concrete implementation
class GoodWeatherDecorator: CellDecorator {
let goodLabel
let goodTextField
let goodButton
override top() {
super.top()
addElement(goodLabel)
}
override middle(){
super.middle()
addElement(goodTextfield)
}
override bottom(){
super.bottom()
addElement(goodButton)
}
}
The final implementation of the cellForRowAt method looks like below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
var decoratedCell = CellDecorator(cell: cell)
switch weather {
case good:
decoratedCell = GoodWeatherDecorator(cell: cell)
case bad:
decoratedCell = BadWeatherDecorator(cell: cell)
}
decoratedCell.configure() // <------------ here is the configure call
cell = decoratedCell.getCell() // <------- here I get cell from the decorator
return cell
}
}
Now I do understand that my implementation of the decorator pattern is not 100% valid, because I don't inherit from the BlueCell class, for example. But that does not bother me that much. The things that bothers me is that I think that this solution to the problem is kind of overkill.
All works the right way, but I can help the feeling of having done too much to solve this trivial problem.
What do you think? How would you solve this kind of problem?
Thanks in advace