Configuring UITableViewCell instances for various models without using conditionals - ios

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.

Related

Generic function to return a UITableViewCell subclass type

I've written an extension to easily dequeue table view cells of a certain type:
class RedCell: UITableViewCell { }
class BlueCell: UITableViewCell { }
extension UITableView {
func dequeueReusableCell<T: UITableViewCell>(_ type: T.Type, for indexPath: IndexPath) -> T {
let identifier = String(describing: T.self) // Must set on Storyboard
return dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! T
}
}
This makes dequeuing cells of the correct type very easy:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(RedCell.self, for: indexPath) // type: RedCell
}
Now, rather than typing RedCell.self into that function call, I would like to store this SomeCell.self on a variable so that each of the enum cases can pass their own custom cell subclass to the table view:
enum Color {
case red, blue
// ...?
// func cellType<T: UITableViewCell>() -> T.Type {
// func cellType<T>() -> T.Type where T: UITableViewCell {
func cellType<T>() -> T.Type {
switch self {
case .red: return RedCell.self // Cannot convert return expression of type 'RedCell.Type' to return type 'T.Type'
case .blue: return BlueCell.self // Cannot convert return expression of type 'BlueCell.Type' to return type 'T.Type'
}
}
}
Desired result is to construct the cell via the enum case:
let color = Color.red
let cell = tableView.dequeueReusableCell(color.cellType(), for: indexPath) // type: UITableViewCell
It's fine for the return type of the above call to be upcast to the in-common UITableViewCell, rather than a subclass. But the cellType() should dequeue the proper cell subclass from the Storyboard, as shown in the first code block, which is based on a String of the class name.
Is this possible? Xcode gives the errors above for my attempts to write the function.
What is the correct syntax for the generic function I'm attempting to write?
No need for generics; just return UITableViewCell.Type:
enum Color {
case red, blue
func cellType() -> UITableViewCell.Type {
switch self {
case .red: return RedCell.self
case .blue: return BlueCell.self
}
}
}
Im not sure I'm a fan of the idea but have a look at this, just use a private static variable and set it the desired type.
This way you avoid the inferring error from the return statement, and still change the type according to your enum case.
enum Color {
private static var cellType = UITableViewCell.self
case red, blue
private func setCellType(){
switch self{
case .red:
Color.cellType = RedCell.self
case .blue:
Color.cellType = BlueCell.self
}
}
func cellType<T: UITableViewCell>() -> T.Type {
setCellType()
return Color.cellType as! T.Type
}
}
class Test {
init(){
print(String(describing: Color.red.cellType()))
}
}
let test = Test()

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

Register and dequeue UITableViewCell of specific type

UITableView was build when objc was already 'old' and swift was never mentioned. In objc there was no need if you dequeued a cell to cast it, you just assigned it and everything went right as long as you had the correct cell.
With swift the power of generics came to the iOS (and other apple) platform(s).
Now I find myself writing a lot of boilerplate again and again (define an identifier, cast the cell, use force unwrap of fatal error,..).
So I wondered if there are ideas that could make this easier to use and cleaner in the code.
An easy way to solve this is by writing a small extension.
This solution will result in a fatalError if a cell is dequeued that has not been registered. This is already the default behaviour for iOS if we call dequeueReusableCell(withIdentifier:for:) if we have not registered the cell.
To let this work we need a way to create a unique identifier for any type of cell that we will register and dequeue using generics. If you would want to have a way to dequeue the same cell for different identifiers then you will have to fallback to the default system (never had any need for that).
So lets make a class named UITableView+Tools.swift (or what ever you like to name it).
extension UITableView {
private func reuseIndentifier<T>(for type: T.Type) -> String {
return String(describing: type)
}
public func register<T: UITableViewCell>(cell: T.Type) {
register(T.self, forCellReuseIdentifier: reuseIndentifier(for: cell))
}
public func register<T: UITableViewHeaderFooterView>(headerFooterView: T.Type) {
register(T.self, forHeaderFooterViewReuseIdentifier: reuseIndentifier(for: headerFooterView))
}
public func dequeueReusableCell<T: UITableViewCell>(for type: T.Type, for indexPath: IndexPath) -> T {
guard let cell = dequeueReusableCell(withIdentifier: reuseIndentifier(for: type), for: indexPath) as? T else {
fatalError("Failed to dequeue cell.")
}
return cell
}
public func dequeueReusableHeaderFooterView<T: UITableViewHeaderFooterView>(for type: T.Type) -> T {
guard let view = dequeueReusableHeaderFooterView(withIdentifier: reuseIndentifier(for: type)) as? T else {
fatalError("Failed to dequeue footer view.")
}
return view
}
}
So now all we have to do in our class (i.e. view controller) is register the cell (no identifier needed) and dequeue it(no identifier, no casting, force unwrapping or manual unwrap with a guard)
func viewDidLoad {
...
tableView.register(MyCustomCell.self)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = dequeueReusableCell(forType: MyCustomCell.self, for: indexPath)
cell.viewModel = cellModel(for: indexPath)
return cell
}
And that's it. Hope you like the idea. Any other (better or worse 😉) ideas are welcome.
#Saren your solution worked fine for single UITableViewCell but I have done some enhancements on it which supports multiple UITableViewCell registrations.
UITableView+extension.swift
public protocol ClassNameProtocol {
static var className: String { get }
var className: String { get }
}
public extension ClassNameProtocol {
public static var className: String {
return String(describing: self)
}
public var className: String {
return type(of: self).className
}
}
extension NSObject: ClassNameProtocol {}
public extension UITableView {
public func register<T: UITableViewCell>(cellType: T.Type) {
let className = cellType.className
let nib = UINib(nibName: className, bundle: nil)
register(nib, forCellReuseIdentifier: className)
}
public func register<T: UITableViewCell>(cellTypes: [T.Type]) {
cellTypes.forEach { register(cellType: $0) }
}
public func dequeueReusableCell<T: UITableViewCell>(with type: T.Type, for indexPath: IndexPath) -> T {
return self.dequeueReusableCell(withIdentifier: type.className, for: indexPath) as! T
}
public func registerHeaderFooter<T: UITableViewHeaderFooterView>(HeaderFooterType: T.Type) {
let className = HeaderFooterType.className
let nib = UINib(nibName: className, bundle: nil)
register(nib, forHeaderFooterViewReuseIdentifier: className)
}
public func registerHeaderFooter<T: UITableViewHeaderFooterView>(HeaderFooterTypes: [T.Type]) {
HeaderFooterTypes.forEach { registerHeaderFooter(HeaderFooterType: $0) }
}
}
Use like
fileprivate let arrCells = [ContactTableViewCell.self,ContactDetailsCell.self]
self.register(cellTypes: arrCells)
Note: Please ensure your class name and reusableidenfer of UITableviewCell should be the same.

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