Generic UITableview Cell - ios

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

Related

Configuring UITableViewCell instances for various models without using conditionals

I'd like to handle many different data types each of which has an associated UITableViewCell class and dedicated configure(for:) function.
Currently my view controller has something like this
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
func failure() {
fatalError("The row type was unknown, or the row item couldn't be cast as expected, or the specified cell couldn't be dequeued, or the dequeued cell couldn't be cast")
}
let section = sections[indexPath.section]
let item = section.rowItems[indexPath.row]
switch item.moduleType {
case .message:
guard let message = item as? CardModules.Message,
let messageCell = tableView.dequeueReusableCell(withIdentifier: CardCell_Message.identifier) as? CardCell_Message
else { failure(); break }
messageCell.configure(for: message)
return messageCell
case .availability:
guard let availability = item as? CardModules.Availability,
let availabilityCell = tableView.dequeueReusableCell(withIdentifier: CardCell_Availability.identifier) as? CardCell_Availability
else { failure(); break }
availabilityCell.configure(for: availability)
return availabilityCell
// etc, for many data types
I'd prefer a system where any model can have it's cell class instantiated with a single call in the view controller, and have written the following solution
protocol DataForCell {
associatedtype Cell: CellForData
func configuredCell(from tableView: UITableView) -> Cell
}
extension DataForCell {
func cell(from tableView: UITableView) -> Cell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell
return cell
}
}
protocol CellForData {
associatedtype Data: DataForCell
static var identifier: String { get }
func configure(for data: Data)
}
Which allows the cellForRowAt to just call configuredCell(from: tableView) on each item, as the array of items is an array of DataForCell
The question is, how can I improve further on this? It would be great if the configuredCell function could be moved into a protocol extension to avoid the need to keeping it as boilerplate in every instance of DataForCell
Finally, does this approach violate any important principles?
Update
Based on Sweepers suggestion, the improved protocols are this:
// MARK: - Data Protocol
protocol DataForCell {
associatedtype Cell: CellForData
}
extension DataForCell where Cell.Data == Self {
func configuredCell(from tableView: UITableView) -> Cell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier) as! Cell
cell.configure(for: self)
return cell
}
}
// MARK: - Cell Protocol
protocol CellForData {
associatedtype Data: DataForCell
static var identifier: String { get }
func configure(for data: Data)
}
Use different cell type all of them based on a common cell class.
class BaseCellType : UITableviewCell {
func configure(…) {
print(“Not implemented”)
}
}
class MessageCell : BaseCellTyoe {
override func configure(…) {
… configure the message cell
}
}
Then create a dictionary of cell identifiers in the TableViewDataSource :
let cellId : [ModuleType:String] = [.message : CardModule.message, …]
Then in cellForRow , dequeue cell with correct identifier for type BaseCellType and call configure.
In storyboard set the final cell type for each cell.
Note : you may also do this with a protocole.

How should I refactor my custom UITableView to improve maintainability

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)
}

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

How can I use an if statement to set a type of variable to a specific type of custom tableCell?

I have three different types of custom UITableCells. I have an if statement that sets them up:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if somePosts[indexPath.row].typeOfPost == .linkPost {
let cell: LinkTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "linkTableViewCell") as! LinkTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .picturePost {
let cell: PictureTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "pictureTableViewCell") as! PictureTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .textPost {
let cell: TextTableViewCell = self.tableView.dequeueReusableCell(withIdentifier: "textTableViewCell") as! TextTableViewCell
} else {
print("Type of post is not link, picture, or text")
}
}
Each of the custom cells has similar labels such as title and time. I would like to set these labels using the same line of code, such as:
cell.titleLabel.text = "Some title here"
However, in this example, I get an error saying I am using an unresolved identifier "cell," obviously because my variables are being declared non-globally. Is there a way around this since swift is strongly typed? Thanks!
Make a protocol that your TableViewCell classes extend, and store cell as a variable of that type.
protocol MyTableViewCell {
var titleLabel: UILabel { get }
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier: String
switch somePosts[indexPath.row].typeOfPost {
case .linkPost: identifier = "linkTableViewCell"
case .picturePost: identifier = "pictureTableViewCell"
case .textPost: identifier = "textTableViewCell"
default: fatalError("Type of post is not link, picture, or text")
}
guard let cell = self.tableView.dequeueReusableCell(withIdentifier: identifier) as? MyTableViewCell else {
fatalError("Cell isn't castable to MyTableViewCell")
}
cell.titleLabel.text = "Some title here"
// ...
}
You have three basic solutions.
Repeat cell.text = ... inside each block. But this isn't what you really want as stated in your question.
Have your three custom cell classes all extend a common base class. Have this base class define any common properties.
Define a protocol with the common properties and have each of your custom cell classes conform to the protocol.
For options 2 and 3 you would declare a variable of the base/protocol type before the first if statement. Then after the whole if/else block, you can assign any of the common properties.
If you need to update any cell type specific properties, you can do that inside the appropriate block as well.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: BaseTableViewCell?
if somePosts[indexPath.row].typeOfPost == .linkPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "linkTableViewCell") as! LinkTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .picturePost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "pictureTableViewCell") as! PictureTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .textPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "textTableViewCell") as! TextTableViewCell
} else {
print("Type of post is not link, picture, or text")
}
if let cell = cell {
cell.commonProperty = ...
return cell
} else {
return nil // this shouldn't happen but if it does, you have a bug to fix
}
}
If the subclasses each have their own titleLabel property, you will need to make them all conform to a protocol. Let's call it ConfigurableCell.
protocol ConfigurableCell {
var titleLabel: UILabel { get set }
}
Then, you can initialize your cells all the same way, but declare them as a ConfigurableCell:
var cell: ConfigurableCell? = nil // not set yet
if somePosts[indexPath.row].typeOfPost == .linkPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "linkTableViewCell") as! LinkTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .picturePost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "pictureTableViewCell") as! PictureTableViewCell
} else if somePosts[indexPath.row].typeOfPost == .textPost {
cell = self.tableView.dequeueReusableCell(withIdentifier: "textTableViewCell") as! TextTableViewCell
}
guard let cell = cell else {
// how to handle this error case is up to you
print("Type of post is not link, picture, or text")
return UITableViewCell()
}
// now, cell is a ConfigurableCell with a titleLabel property, regardless of class
cell.titleLabel.text = "Some title"
Of course, UITableViewCell does have a built-in textLabel property, which you could try to utilize in your cell classes, and then a protocol wouldn't be necessary, because the property is in UITableViewCell.

how to create class reference using String

so here's the problem that I'm facing.
Take a look
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExpenseTableViewCell_Title") as! ExpenseTableViewCell_Title
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "ExpenseTableViewCell_SaveCanel") as! ExpenseTableViewCell_SaveCanel
return cell
}
}
what i want to do is, use cell identifier string as cell type.(i.e. ExpenseTableViewCell_Title, ExpenseTableViewCell_SaveCanel).
I do have cell identifier array.
var TableCell:[ExpenseCellType] = [.ExpenseTableViewCell_Title, .ExpenseTableViewCell_SaveCanel]
Right now I only have two types of cell. But this number will go high.
And I don't want to use if/else condition or switch case.
Thank in advance.
Can make it shorter with extension:
extension UITableView {
func dequeueReusable<T>(type: T.Type, index: IndexPath) -> T {
return self.dequeueReusableCell(withIdentifier: String(describing: T.self), for: index) as! T
}
}
Use it like this, will return ExpenseTableViewCell_Title type cell:
let cell = tableView.dequeueReusable(type: ExpenseTableViewCell_Title.self, index: indexPath)
Just store your class in the array like [ExpenseTableViewCell_Title.self, ExpenseTableViewCell_SaveCanel.self] and pass it to this function
You can use function NSClassfromString but you will need namespace for getting class from String.
I have created example here to use it.
Example:
func getClassFromString(_ className: String) -> AnyClass! {
let namespace = Bundle.main.infoDictionary!["CFBundleExecutable"] as! String;
let cls: AnyClass = NSClassFromString("\(namespace).\(className)")!;
return cls;
}
class customcell: UITableViewCell {
}
let requiredclass = getClassFromString("customcell") as! UITableViewCell.Type
let cellInstance = requiredclass.init()

Resources