Reputation: 63
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
Upvotes: 0
Views: 1487
Reputation: 5379
You should follow Daniel T.'s answer.
But here's a suggested upgrade that I use on my own projects.
Instead of just using
protocol ConfigurableCell {
func configure(with weather: Weather)
}
I use this for reusability purposes in many different scenarios.
protocol Configurable {
associatedtype Initializables
func configure(_ model: Initializables) -> Self
}
Example use cases:
UIViewController
class SomeViewController: UIViewController {
var someIntProperty: Int?
...
}
extension SomeViewController: Configurable {
struct Initializables {
let someIntProperty: Int?
}
func configure(_ model: SomeViewController.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
return self
}
}
// on some other part of the code.
let someViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! SomeViewController
_ = someViewController.configure(SomeViewController.Initializables(someIntProperty: 100))
UITableViewCell
class SomeTableViewCell: UITableViewCell {
var someIntProperty: Int?
var someStringProperty: Int?
...
}
extension SomeTableViewCell: Configurable {
struct Initializables {
let someIntProperty: Int?
let someStringProperty: Int?
}
func configure(_ model: SomeTableViewCell.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
self.someStringProperty = model.someStringProperty
return self
}
}
// on cellForRow
let cell = tableView.dequeueReusableCell(withIdentifier: "SomeTableViewCell", for: indexPath) as! SomeTableViewCell
return cell.configure(SomeTableViewCell.Initializables(someIntProperty: 100, someStringProperty: "SomeString"))
Notes:
As you can see it's very reusable and easy to use and implement. Downside is the code generated could be long when using configure
Upvotes: 0
Reputation: 33979
Given that you only show two types of cells and your solution doesn't actually get rid of the switch statement, I'd say that your solution counts as "overkill."
You don't show it, but it seems that you have a Weather
enum. I'll assume that...
enum Weather: String {
case good
case bad
}
In the table view datasource, my goal would be to have something like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let weather = weathers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: weather.rawValue, for: indexPath) as! ConfigurableCell
cell.configure(with: weather)
return cell as! UITableViewCell
}
In order to achieve the above, I would have several cells laid out in the storyboard file with different identifiers. I would have a subclass for each type of cell in my code where all of them conform to the ConfigurableCell
protocol.
protocol ConfigurableCell {
func configure(with weather: Weather)
}
If you can't conform your Weather
enum to the String type, you will need a switch statement to convert a weather object to a string identifier, but otherwise, no switch statements necessary.
Upvotes: 1