How should I refactor my custom UITableView to improve maintainability - ios

I've got a UITableView with many different kind of views. In each method of the UITableView data source I need to check the type of the cell and type of the object, cast them, and act correctly. This is not very clean (it works) but not very maintainable.
So I was working on something to abstract this part but I'm a little bit stuck. The following code is simplified and maybe not that useful but it is to demonstrate my current problem:
extension UITableView {
func dequeue<T: UITableViewCell>(_ type: T.Type,
for indexPath: IndexPath) -> T {
let cell = dequeueReusableCell(withIdentifier: String(describing: type),
for: indexPath)
guard let cellT = cell as? T else {
fatalError("Dequeue failed, expect: \(type) was: \(cell)")
}
return cellT
}
}
struct Row<Model, Cell> {
let view: Cell.Type
let model: Model
var fill: ((Model, Cell) -> Void)
}
// Completly unrelated models
struct Person {
let name: String
}
struct Animal {
let age: Int
}
// Completely unrelated views
class PersonView: UITableViewCell {
}
class AnimalView: UITableViewCell {
}
// Usage:
let person = Person(name: "Haagenti")
let animal = Animal(age: 12)
let personRow = Row(view: PersonView.self, model: person) { person, cell in
print(person.name)
}
let animalRow = Row(view: AnimalView.self, model: animal) { animal, cell in
print(animal.age)
}
let rows = [
// personRow
animalRow
]
let tableView = UITableView()
for row in rows {
tableView.register(row.view, forCellReuseIdentifier: String(describing: row.view))
let indexPath = IndexPath(row: 0, section: 0)
let cell = tableView.dequeue(row.view, for: indexPath)
row.fill(row.model, cell)
}
The code works, but when I enable the animalRow Swift will complain. This is not that surprising since it cannot resolve the types. I cannot figure out how to get around this.
By using the following code I can declare everything once and execute all the parts like "fill" when I need them. I will also add code like onTap etc, but I removed all this code to keep to problem clear.

Sahil Manchanda's answer is covering the OOD approach to solving this problem but as a drawback you have to define your models as class.
First thing we need to consider is the fact that we're discussing about maintainability here, so in my humble opinion, Model should not know about the view (or which views it's compatible with), That is Controller's responsibility. (what if we want to use the same Model for another view somewhere else?)
Second thing is that if we want to abstract it to higher levels, it will definitely require down-cast/force-cast at some point, so there is a trade-off to how much it can be abstracted.
So for sake of maintainability, we can increase the readability and separation of concern/local reasoning.
I suggest to use an enum with associatedValue for your models:
enum Row {
case animal(Animal)
case person(Person)
}
Well right now our Models are separated and we can act differently based on them.
Now we have to come-up with a solution for Cells, I usually use this protocol in my code:
protocol ModelFillible where Self: UIView {
associatedtype Model
func fill(with model: Model)
}
extension ModelFillible {
func filled(with model: Model) -> Self {
self.fill(with: model)
return self
}
}
So, we can make our cells conform to ModelFillible:
extension PersonCell: ModelFillible {
typealias Model = Person
func fill(with model: Person) { /* customize cell with person */ }
}
extension AnimalCell: ModelFillible {
typealias Model = Animal
func fill(with model: Animal) { /* customize cell with animal */ }
}
Right now we have to glue them all together. We can refactor our delegate method tableView(_, cellForRow:_) just like this:
var rows: [Row] = [.person(Person()), .animal(Animal())]
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch rows[indexPath.row] {
case .person(let person): return (tableView.dequeue(for: indexPath) as PersonCell).filled(with: person)
case .animal(let animal): return (tableView.dequeue(for: indexPath) as AnimalCell).filled(with: animal)
}
}
I believe in future this is more readable/maintainable than down-casting in Views or Models.
Suggestion
I also suggest to decouple PersonCell from Person too, and use it like this:
extension PersonCell: ModelFillible {
struct Model {
let title: String
}
func fill(with model: Model { /* customize cell with model.title */ }
}
extension PersonCell.Model {
init(_ person: Person) { /* generate title from person */ }
}
And in your tableView delegate use it like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch rows[indexPath.row] {
case .person(let person): return (tableView.dequeue(for: indexPath) as PersonCell).filled(with: .init(person))
case .animal(let animal): return (tableView.dequeue(for: indexPath) as AnimalCell).filled(with: .init(animal))
}
}
With current approach compiler will always know what's going on, and will block you from making mistakes & in future by reading this code, you know exactly what's going on.
Note
The reason that it will require down-cast/force-cast at some point if we try to abstract it to higher levels (just like Sahil's answer), is the fact that dequeue does not happen at the same-time we want to fill/customize our cell. dequeue has to return a type known to compiler. it's either UITableViewCell, PersonCell or AnimalCell. In first case we have to down-cast it, and it's not possible to abstract PersonCell and AnimalCell (unless we try down-cast/force-cast in their models). We can use a type like GenericCell<Row> and also cell.fill(with: row) but that means that our customized cell, has to handle all cases internally (it should handle PersonCell and AnimalCell views at the same time which is also not maintainable).
Without down-cast/force-cast this is the best I got to over the years. If you need more abstractions (single line for dequeue, and a single line for fill) Sahil's answer is the best way to go.

Have a look at the following struct:
protocol MyDelegate {
func yourDelegateFunctionForPerson(model: Person)
func yourDelegateFunctionForAnimal(model: Animal)
}
enum CellTypes: String{
case person = "personCell"
case animal = "animalCell"
}
Base Model
class BaseModel{
var type: CellTypes
init(type: CellTypes) {
self.type = type
}
}
Person Model
class Person: BaseModel{
var name: String
init(name: String, type: CellTypes) {
self.name = name
super.init(type: type)
}
}
Animal Model
class Animal: BaseModel{
var weight: String
init(weight: String, type: CellTypes) {
self.weight = weight
super.init(type: type)
}
}
Base Cell
class BaseCell: UITableViewCell{
var model: BaseModel?
}
Person Cell
class PersonCell: BaseCell{
override var model: BaseModel?{
didSet{
guard let model = model as? Person else {fatalError("Wrong Model")}
// do what ever you want with this Person Instance
}
}
}
Animal Cell
class AnimalCell: BaseCell{
override var model: BaseModel?{
didSet{
guard let model = model as? Animal else {fatalError("Wrong Model")}
// do what ever you want with this Animal Instance
}
}
}
View Controller
class ViewController: UIViewController{
#IBOutlet weak var tableView: UITableView!
var list = [BaseModel]()
override func viewDidLoad() {
super.viewDidLoad()
setupList()
}
func setupList(){
let person = Person(name: "John Doe", type: .person)
let animal = Animal(weight: "80 KG", type: .animal)
list.append(person)
list.append(animal)
tableView.dataSource = self
}
}
extension ViewController: UITableViewDataSource{
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = list[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: model.type.rawValue, for: indexPath) as! BaseCell
cell.model = model
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return list.count
}
}
extension ViewController: MyDelegate{
func yourDelegateFunctionForPerson(model: Person) {
}
func yourDelegateFunctionForAnimal(model: Person) {
}
}
MyDelegate protocol is used to perform "Tap" actions
CellTypes enums is used to identify Cell Type and for dequeuing
All of the Model class will inherit BaseModel which is quite useful and will eliminate the need to typecase in cellForRow at function. and All the tableViewCells have inherited BaseCell which holds two variables i.e. model and delegate. these are overridden in Person and Animal Cell.
Edit: Risk of losing Type Safety can certainly be reduced if you specify the 'celltype' directly in super.init() in model class. e.g.
class Person: BaseModel{
var name: String
init(name: String) {
self.name = name
super.init(type: .person)
}
}
As cells are being dequeued with 'type' variable.. correct model will be supplied to correct cell.

I would create a protocol for the rows to be used in the data source array
protocol TableRow {
var view: UITableViewCell.Type {get}
func fill(_ cell: UITableViewCell)
}
And then create different row structs that conforms to this protocol
struct PersonRow: TableRow {
var view: UITableViewCell.Type
var model: Person
func fill(_ cell: UITableViewCell) {
cell.textLabel?.text = model.name
}
}
struct AnimalRow: TableRow {
var view: UITableViewCell.Type
var model: Animal
func fill(_ cell: UITableViewCell) {
cell.textLabel?.text = String(model.age)
}
}
Then the data source would be defined as
var rows: [TableRow]()
and any type conforming to the TableRow protocol can be added
rows.append(PersonRow(view: PersonView.self, model: person))
rows.append(AnimalRow(view: AnimalView.self, model: animal))
and setting values for a cell would be done by calling fill
let cell = tableView.dequeue(row.view, for: indexPath)
row.fill(cell)

I understand what you want to implement. There is a small library in Swift for this thing. https://github.com/maxsokolov/TableKit
The most interesting part here for you is ConfigurableCell, it will solve your problem if you will just copy this protocol to your project:
https://github.com/maxsokolov/TableKit/blob/master/Sources/ConfigurableCell.swift
Basic idea is following:
public protocol ConfigurableCell {
associatedtype CellData
static var reuseIdentifier: String { get }
static var estimatedHeight: CGFloat? { get }
static var defaultHeight: CGFloat? { get }
func configure(with _: CellData)
}

Related

Reducing repeated code with tableviews existing in classes of different types in Swift

I have two classes with basically identical tableViewDelegate and tableViewDataSource implementation code. In the interest of leaving the code better than I found it, I figure I should try to reduce duplication. This is proving to be quite difficult. See the example code structure below:
class A : UICollectionViewCell {
//a bunch of code
}
extension A : UITableViewDataSource, UITableViewDelegate {
//A bunch of code that is nearly identical to the other class
}
class B : UIViewController {
//a bunch of code
}
extension B : UITableViewDataSource, UITableViewDelegate {
//A bunch of code that is nearly identical to the other class
}
Both extensions use the same global variables from class A and B respectively. My initial idea was to create a superclass for class A and B that already has these delegates implemented. However, I don't think this will work because classes A and B are not extending the same class. I think I would have to go too far up the class hierarchy to find a superclass that they share.
Is there a good way to reduce this repeated code?
Thanks
You could try to create a shared UITableViewDataSource and UITableViewDelegate class that manages the data for both table views. In the example below a ViewModel that is generic over Item, can manage lists of Book or User instances. They share a CellModel interface that is used to configure the cell for the table view.
import UIKit
class ViewController: UITableViewController {
struct User: CellModel {
let displayName: String
var title: String { displayName }
}
let viewModel = ViewModel<User>(
items: [
.init(displayName: "Hanna"),
.init(displayName: "Jo")
]
)
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = viewModel
tableView.delegate = viewModel
}
}
// MARK: -
class AnotherViewController: UITableViewController {
struct Book: CellModel {
let title: String
}
let viewModel = ViewModel<Book>(
items: [
.init(title: "Stories from A"),
]
)
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = viewModel
tableView.delegate = viewModel
}
}
// MARK: -
protocol CellModel: Hashable {
var title: String { get }
}
final class ViewModel<Item: CellModel>: NSObject, UITableViewDataSource, UITableViewDelegate {
let items: [Item]
init(items: [Item]) {
self.items = items
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "CellID") else {
return UITableViewCell(style: .default, reuseIdentifier: "CellID")
}
let item = items[indexPath.row]
var content = cell.defaultContentConfiguration()
content.text = item.title
cell.contentConfiguration = content
return cell
}
}

Unit Test for TableView isSelected Scenario Swift

I have a simple scenario and tableView.
I need to select 3 cells and and even I scroll it will be stay like. I can select 1, 2 or 3 cells but not 4 or more.When I select 4 cells nothing should happen. If there are 3 selected cells and I click one of them the cell which I selected should be deselected. So far so good. Thanks to #vadian help I can make this happen as below.
But I tried all day writing unit test for this scenario but could not handle it.. I decided to ask help community and still could not find a way..
Question is: How can I write unit test with a nice approach for this scenario ?
/// My Model ///
struct Item: Codable {
let name: String
let image: String
var isSelected = false
enum CodingKeys: String, CodingKey {
case name = "name"
case image = "image"
}
}
// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
let items = [Item]() // fetched from network, there is item objects inside it.
// MARK: - UITableViewDataSource
extension ItemViewModel: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Food", for: indexPath) as! FoodTableViewCell
let item = self.items[indexPath.row]
cell.isSelected = item.isSelected
return cell
}
}
}
Of course, you can write many unit tests for tableView actions but I agree with Matt in terms of "this should be simple". Your ViewModel needs to be handled according to "Separate Concern" principles otherwise it needs to be refactored. I assume you move all logic into ViewModel then you can write a unit test as below.
import Foundation
class YourViewControllerTest: XCTestCase {
var viewModel: ItemViewModel!
fileprivate var fetcher: MockItemFetcher!
var tableView: UITableView!
override func setUp() {
super.setUp()
fetcher = MockItemFetcher()
viewModel = ItemViewModel(fetchable: fetcher)
}
override func tearDown() {
viewModel = nil
fetcher = nil
tableView = nil
super.tearDown()
}
/// test the one cell can be selectable or not
func test_did_select_a_cell() {
let items = [Item(name: "", image: "",)]
// given
fetcher.items = items
viewModel.fetchItems()
let viewController = YourViewController()
let tableView = UITableView()
viewController.viewModel = viewModel
// when
viewController.tableView(tableView, didSelectRowAt: IndexPath(row: 0, section: 0))
// then
XCTAssertNotNil(willBeSelected)
}

How to make Base class property generic in swift so can use with multiple model type?

I want the property "cellViewModel" as generic so I can reuse BaseCustomCell with a different types of models.
Ex.
struct CELLVIEWMODEL {
var name: String
var address: String
}
class BaseCustomCell: UITableViewCell {
var cellViewModel: CELLVIEWMODEL //should support different model types CELLVIEWMODEL1,CELLVIEWMODEL2
{
didSet() {
setValuesInSubClasses
}
}
func setValuesInSubClasses() {
//subclass will implement
}
}
class subCell: BaseCustomCell {
override func setValuesInSubClasses() {
//set value from cellViewModel
}
}
//This is how i am setting from cellForRowAtIndexPath method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: viewModel.getCellId(), for: indexPath) as! BaseCustomCell
cell.cellViewModel = viewModel.getCellModelAtIndexPath(indexPath) //this will set values for subclasses
return cell
}
Now, I am creating new BaseCustomCell each time for different types of cellViewModel. Could you help with any solution?
There are several ways to achieve your goals.
You can make the actual BaseCustomCell glass generic, but be aware if you use Storyboards, this isn't a solution, since you'd need to hardcode the generic type into the storyboard.
The other solution that works with storyboards too is to declare your viewmodel as a protocol, then you can replace it with any concrete implementation of the protocol.
protocol CellViewModel {
var name: String { get }
var address: String { get }
}
class BaseCustomCell: UITableViewCell {
var cellViewModel: CellViewModel {
didSet() {
setValuesInSubClasses
}
}
func setValuesInSubClasses() {
//subclass will implement
}
}
class SubCell: BaseCustomCell {
override func setValuesInSubClasses() {
//set value from cellViewModel
}
}
And then your viewModel.getCellModelAtIndexPath should have a return type of CellViewModel, so it can return any type that conforms to the protocol.
So you simply need to declare your concrete cell view models like class FirstCellViewModel: CellViewModel { ... }, etc. and you can return them from getCellModelAtIndexPath

Generic UITableview Cell

I want to create a tableview with customs cells, each cell is different. But i want the implementation to be so smart.
I thought instead of making if else check in cellForRow:atIndex: for every cell like this
if indexpath.section == 0 {
// Create first cell
} else if indexpath.section == 1 {
// Create second cell
}
and so on..
I decided to create a protocol like so:
protocol tableviewProtocol {
func cellForAt(index: IndexPath, object: ObjectModel) -> UItableviewCell
}
and for every cell i want to show i create a class like so:
class firstCellClass: tableviewProtocol {
func cellForAt(index: IndexPath, object: ObjectModel) -> UItableviewCell {
// create and return the cell
}
}
and created this array,
var tableviewDataSource: [tableviewProtocol] = []
and then fill the array like so:
tabletableviewDataSource = [firstCellClass(), secondCellClass(), thirdCellClass()]
and in tableView cellForRow:atIndex:
return tableviewDataSource[indexpath.section].cellForAt(indexpath, object: object)
And that was fine until i have to create different cell with different object model type instead of ObjectModel
So how can i make it more generic that can accept any type of objects ?
You can make it more generic by subclassing UITableViewCell
class firstCellClass: UITableViewCell {
}
class secondCellClass: UITableViewCell {
}
class thirdCellClass: UITableViewCell {
}
let arrayOfCells = [UITableViewCell]()
Something like this should point you in the right direction. There are many ways to achieve this goal
I realize this isn’t the most elegant way to do it but hopefully it helps you.
Let’s say you have three types:
struct ModelA { ... }
struct ModelB { ... }
struct ModelC { ... }
So you can make the array of type Any:
var model = [[Any]]() // append data
Then in cellForRow:
var cell: UITableViewCell
let value = model[indexPath.section][indexPath.row]
switch val {
case is ModelA:
cell = tableView.dequeueReusableCell(withIdentifier: "cellA", for: indexPath) as! CellClassA
case is ModelB:
cell = tableView.dequeueReusableCell(withIdentifier: "cellB", for: indexPath) as! CellClassB
case is ModelC:
cell = tableView.dequeueReusableCell(withIdentifier: "cellC", for: indexPath) as! CellClassC
default:
cell = UITableViewCell()
}
return cell
Nighttalker had a good answer to just use "Any" but you could also try 2 other ways:
Make ObjectModel a protocol, and make all objects you want to pass in implement that protocol
protocol ObjectModel{
}
class SomeObject: ObjectModel{}
Use generics
protocol tableviewProtocol {
associatedtype ObjectModel
func cellForAt(index: IndexPath, object: ObjectModel) -> UITableViewCell
}
class SomeObject: tableviewProtocol{
typealias ObjectModel = String //whatever type you want
func cellForAt(index: IndexPath, object: ObjectModel) -> UITableViewCell{
//return your cell
}
}

How to create dynamic views programmatically with a good OO-design

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
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.
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

Resources