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

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

Related

Handle events in subviews in MVVM in Swift

I am trying to get into MVVM in Swift and I am wondering how to handle events in subviews in MVVM, and how these events can travel up the chain of views/viewmodels. I'm talking about pure Swift for now (no SwiftRx etc.).
Example
Say I have a TableViewController with a TableViewModel. The view model holds an array of objects and creates a TableCellViewModel for each one, since each cell represents one of these objects. The TableViewController gets the number of rows to display from its model and also the view model for each cell, so it can pass it along to the cell.
We then have a TableCell and each cell has a TableCellViewModel. The TableCell queries its model for things like user-facing strings etc.
Now let's say TableCell also has a delete button that delete's that row. I'm wondering how to handle that: Usually, the cell would forward the button press to its view model, but this is not where we need it - we eventually need to know about the button press in either TableViewController or TableViewModel, so we can remove the row from the table view.
So the question is:
How does the button event get from a TableCell upwards in the view chain in MVVM?
Code
As requested in the comments, code that goes with the example:
class TableViewController: UIViewController, UITableViewDataSource {
var viewModel: TableViewModel = TableViewModel()
// setup and such
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.viewModel.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableCell
cell.viewModel = self.viewModel.cellViewModel(at: indexPath.item)
return cell
}
}
class TableViewModel {
// setup, get data from somewhere, ...
var count: Int {
return self.modelObjects.count
}
func cellViewModel(at index: Int) -> TableCellViewModel {
let modelObject = self.modelObjects[index]
let cellViewModel = TableCellViewModel(modelObject: modelObject)
return cellViewModel
}
}
class TableCell {
var viewModel: TableCellViewModel!
// setup UI, do what a cell does
func viewModelChanged() {
self.titleLabel.text = self.viewModel.title()
}
func deleteButtonPressed(_ sender: UIButton) {
// Oh, what to do, what to do?
}
}
class TableCellViewModel {
private var modelObject: ModelObject
init(modelObject: ModelObject) {
self.modelObject = modelObject
}
func title() -> String {
return self.modelObject.title
}
}
TableViewModel is the source of truth, so all global operations should be performed in there. Pressing a button is completely UI operation and viewModel shouldn't handle this in direct way.
So, for now we know two facts:
TableViewModel should delete the cell from array and then viewController should handle the deletion animation process;
Button press shouldn't be handled in child viewModel.
According to this you can achieve it by:
Pass button pressed event up to viewController (use callback or delegate pattern);
Call TableViewModel method to delete specific cell:
viewModel.deleteCell(at: indexPath)
Properly handle deletion animation in viewController.
may be you can use nextResponder util nextResponder is VC, and VC responder to delegate (eg:CellEventDelegate) that handle delete data and cell
UIResponder *nextResponder = pressedCell.nextResponder;
while (nextResponder) {
if ([nextResponder conformsToProtocol:#protocol(CellEventDelegate)]) {
if ([nextResponder respondsToSelector:#selector(onCatchEvent:)]) {
[((id<CellEventDelegate>)nextResponder) onCatchEvent:event];
}
break;
}
nextResponder = nextResponder.nextResponder;
}

UISegmentedControl in each UITableViewCell of UITableView

I'm trying to implement UISegmentedControl in each dequeueReusableCell UITableViewCell like so:
The Issue: Each TableViewCell is referencing to the same Segmented Control and I'm unable to fetch the state of the control for any cell in particular. As per my understanding, there's only one instance of SegmentedControl that is being initialised and that instance is being shared by all the TableViewCells, and because of that I can't access the unique value of the state for any particular TableViewCell, eg: I'm unable to access what the SegmentControl state is set to for the 3rd cell.
View Controller Code:
import UIKit
import UserNotifications
class MarkAttendanceViewController: UIViewController {
var pickedDate: Date = Date.init()
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 13.0, *) {
overrideUserInterfaceStyle = .light
}
}
#IBAction func datePicker(_ sender: UIDatePicker) {
pickedDate = sender.date.addingTimeInterval(19800)
let weekDay = Calendar(identifier:.gregorian).component(.weekday, from: pickedDate)
print(weekDay)
updateSubjects(pickedDate)
}
func updateSubjects(_ pickedDate: Date) {
}
}
extension MarkAttendanceViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "subjectCell", for: indexPath) as! SubjectTableViewCell
cell.SessionType.text = "Lecture"
cell.SessionName.text = "Network Security"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
class SubjectTableViewCell: UITableViewCell {
#IBOutlet var SessionType: UILabel!
#IBOutlet var SessionName: UILabel!
#IBOutlet var segmentControlOutlet: UISegmentedControl!
#IBAction func segmentedControlIndex(_ sender: UISegmentedControl) {
print(sender.selectedSegmentIndex)
}
}
Github Link here
Please let me know if there's any more information that I need to provide or if the question isn't clear. TIA
You should set the tag of your segmentControlOutlet to indexPath.row in cellForRowAt:IndexPath method.
Also you must add an action on valueChange event on each of your UISegmentedControl in the same method.
below code might give you some idea:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "subjectCell", for: indexPath) as! SubjectTableViewCell
cell.SessionType.text = "Lecture"
cell.SessionName.text = "Network Security"
// add an action on value change to detect a change in the value of segmented control
cell.segmentControlOutlet.addTarget(self, action: #selector(segmentValueChanged(_:)), for: .valueChanged)
// set the tag property of your segmented control to uniquely identify each segmented control in the value change event
cell.segmentControlOutlet.tag = indexPath.row
return cell
}
and you can distinguish among various instances of UISegmentedControl using the tag property that you set inside the cellForRow method.
#objc func segmentValueChanged(_ sender: UISegmentedControl) {
switch sender.tag {
case 0:
// do something on value changed in segmented control in first cell and so on...
print(sender.tag)
default:
break
}
print(sender.selectedSegmentIndex)
}
Hope this helps
Use this toterial. add UITableViewCell to your project and set UISegment action in custom UITableViewCell
it seems that the root cause of the issue that would like to pass the data between the cell and the VC containing the table and this is done simple by delegate and protocol design pattern as below
you will have a protocol defining the data to be passed between two members as below
protocol SubjectTableViewCellDelegate {
func didSelectSegmentControlCell(cell: SegmentCell)
}
then you will have cell containing the segment control and a delegate var of type SegmentControlDelegate as below
import UIKit
class SubjectTableViewCell: UITableViewCell {
// MARK: Properties
var delegate: SubjectTableViewCellDelegate?
// MARK: IBOutlets
#IBOutlet weak var segmentControl: UISegmentedControl!
// MARK: Life Cycle Methods
override func awakeFromNib() {
super.awakeFromNib()
}
// MARK: IB Actions
#IBAction func segmentControlAction(_ sender: UISegmentedControl) {
delegate?.didSelectSegmentControlCell(cell: self)
}
}
then you will have your VC acting as a delegate of the Segment cell after having each cell delegate to be the VC containing the Table
import UIKit
class MarkAttendanceViewController: UIViewController, SegmentCellDelegate, UITableViewDelegate, UITableViewDataSource {
// MARK: SegmentCellDelegate Methods
func didSelectSegmentControlCell(cell: SegmentCell){
// you will have the cell that contains all the data
/* all your business here */
}
extension MarkAttendanceViewController: UITableViewDataSource, UITableViewDelegate {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "subjectCell", for: indexPath) as! SubjectTableViewCell
/* remember to have thee delegate of the cell to self as the below line */
cell.delegate = self
cell.SessionType.text = "Lecture"
cell.SessionName.text = "Network Security"
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
}
}
the idea is a general idea imagine there is a button or date picker or any other outlet you should use this pattern to move data between two sides
I would suggest that the answers you have been given, including the accepted answers, are quick fixes that don't actually address the real problem with how you have architected this piece of software. You may not care at this point, but for future readers of the question this may be helpful.
You may have heard of the Model-View-Controller (MVC) architecture that is commonly used when developing for the iOS platform. In the case of your software you have a View -- for simplicity's sake, let's just consider the table view cells as the view in this case. You have a controller -- your MarkAttendanceViewController which implements the UITableViewDataSource and UITableViewDelegate interfaces. The issue, however, is that you don't really have a model for the data you are displaying in the view. In fact, the root of your problem stems from the fact that you are using the view as the model as well, which is problematic because table view cells are reused and the data contained in them can be lost during the cell reuse process if it is not stored somewhere else. If the data is stored in a data model class, you can keep it separate from the table view cells and it will persist through cell reuse.
You have 3 pieces of data associated with each table view cell: The SessionType, the SessionName and the attendance status for the session (ie: Attended, Missed, Mass Bunk or No Lecture). A data model for this could look like this (with an enumerated type to represent the attendance status):
enum AttendanceStatus: Int {
case attended
case missed
case massBunk
case noLecture
}
struct Session {
let name: String
let type: String
var attendanceStatus: AttendanceStatus
}
You may also want to represent type with an enum, but let's keep this simple.
You can instantiate an instance of this data model as follows:
var session = Session(name: "Network Security", type: "Lecture", attendanceStatus: .attended)
Note the var keyword to make it mutable, as you will want to change the attendanceStatus when the UISegmentedControl value changes. Changing this property is done like so:
session.attendanceStatus = .noLecture
To map from your segmented control to AttendanceStatus, you can use the Int raw value for the enum, as follows:
AttendanceStatus(rawValue: segmentedControl.selectedSegmentIndex)
And to map from your data model's attendanceStatus property to a selectedSegmentIndex for your segmented control:
segmentedControl.selectedSegmentIndex = session.attendanceStatus.rawValue
Now in your view controller, you can instantiate an array of Session objects and use that to populate your table view. When a segmented control changes, you can use the indexPath.row of the table view cell for the segmented control in order to find the Session instance in your array of sessions!
(For a more advanced implementation of this, you can also looking into the Model-View-ViewModel (MVVM) architecture which provides an even cleaner way of bidirectional mapping between the data model and the view)
There are multiple way to solve this Problem.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "subjectCell", for: indexPath) as! SubjectTableViewCell
cell.SessionType.text = "Lecture"
cell.SessionName.text = "Network Security"
// add an action on value change to detect a change in the value of segmented control
cell.segmentControlOutlet.addTarget(self, action: #selector(segmentChanged(_:)), for: .valueChanged)
// set the tag property of your segmented control to uniquely identify each segmented control in the value change event
cell.segmentControlOutlet.tag = indexPath.section
return cell
}
Then Find cell based on Segment Control.
#objc func segmentChanged(_ sender: UISegmentedControl) {
if let cell = sender.superview as! UITableViewCell {
let indexPath = tableView.indexPathForCell(cell)
print(indexPath.row)
if indexPath.row == 0 {
print("segment event of cell 0")
}
else if indexPath.row == 1 {
print("segment event of cell 1")
}
}
}
you can also use delegate and Clouser

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

Removing views from superview in cell

I have a cell class 'NewsCell' (subclass of UITableViewCell) that I use for two different kinds of news: OrganizationNews and ProjectNews. These news has common things, but some of elements are different. Namely, when my cell is used for ProjectNews I want to hide Organization's logo, when it is for OrganizationNews I want to hide Project's name button.
I have 'configureCell(_, forNews, ofProject)' method. I call it in 'NewsViewController'. I used 'removeFromSuperview' method, because I need to rearrange my elements in 'NewsCell'. Changing 'isHidden' value won't give me that effect.
So, that is the issue. I have 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' exception in the lines projectNameButton.removeFromSuperview() or logoImageView.removeFromSuperview().
What should I do?
// NewsViewController.swift
func configureCell(_ cell: NewsCell, forNews news: News, ofProject project: Project? = nil) {
//...
if news is OrganizationNews {
cell.projectNameButton.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView.removeFromSuperview()
}
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell, for: indexPath) as! NewsCell
configureCell(cell, forNews: news)
cell.delegate = self
return cell
}
A UITableView or UICollectionView are built on the reuse concept, where the cells are reused and repopulated when you work on it.
When you try to call dequeReusableCell(withIdentifier:), it sometimes returns something that is created before. So, suppose you dequed before something which had all controls, then removed one (removeFromSuperview), then tried to deque again, the new dequed one may NOT have the subview.
I think the best solution for you is making two different cells.
Example:
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// Put here things that are ONLY for OrganizationNewsCell
}
class ProjectNewsCell: BaseNewsCell {
// Put here things that are ONLY for ProjectNewsCell
}
Then deque them from 2 different identifier by two different storyboard cells, xibs.
Or
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// This happens when this kind of cell is created for the first time
override func awakeFromNib() {
super.awakeFromNib()
someNonCommon.removeFromSuperview()
}
}
class ProjectNewsCell: BaseNewsCell {
override func awakeFromNib() {
super.awakeFromNib()
someOtherNonCommon.removeFromSuperview()
}
}
Note: This violates Liskov's principle (one of the SOLID principles), because you remove functionality from superclass in the subclass.
Change the removing lines as below,
if news is OrganizationNews {
cell.projectNameButton?.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView?.removeFromSuperview()
}
This will fix the issue. But a good approach would be to create separate classes for each cell. You can create a base class to keep common logic there.
You shouldn't remove the subview from the outside of the cell. Let's refactor your code.
NewsCell.swift
final class NewsCell: UITableViewCell {
enum Kind {
case organization
case project
}
var logoImageView: UIImageView?
let nameLabel = UILabel()
var kind: NewsCell.Kind {
didSet {
if kind != oldValue {
setupLogoImageView()
self.setNeedsLayout()
}
}
}
init(kind: NewsCell.Kind, reuseIdentifier: String?) {
self.kind = kind
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Positioning
extension NewsCell {
override func layoutSubviews() {
super.layoutSubviews()
// Your layouting
switch kind {
case .organization:
// Setup frame for organization typed NewsCell
case .project:
// Setup frame for project typed NewsCell
}
}
}
// MARK: - Setup
extension NewsCell {
private func setupLogoImageView() {
logoImageView = kind == .organization ? UIImageView() : nil
}
}
How to use:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell) as? NewsCell
if cell == nil {
cell = NewsCell(kind: .organization, reuseIdentifier: TableViewCellIdentifiers.newsCell)
}
cell!.kind = news is Organization ? .organization: .project
return cell!
}

unit test custom UITableViewCell?

Is it possible to init and load only custom cell and test outlets?
My ViewController has TableView with separated dataSource ( which is subclass of custom data source ). So it's kinda tricky to create cell using all of those.
Custom cell has only a couple of labels and config method for updating them from object, so if loaded, testing would be easy.
It is possible to write a unit test for custom UITableViewCell that will test its outlets and any other functionality included in it. The following sample demonstrates this:
class TestItemTableViewCell: XCTestCase {
var tableView: UITableView!
private var dataSource: TableViewDataSource!
private var delegate: TableViewDelegate!
override func setUp() {
tableView = UITableView(frame: CGRect(x: 0, y: 0, width: 200, height: 400), style: .plain)
let itemXib = UINib.init(nibName: "ItemTableViewCell",
bundle: nil)
tableView.register(itemXib,
forCellReuseIdentifier: "itemCell")
dataSource = TableViewDataSource()
delegate = TableViewDelegate()
tableView.delegate = delegate
tableView.dataSource = dataSource
}
func testAwakeFromNib() {
let indexPath = IndexPath(row: 0, section: 0)
let itemCell = createCell(indexPath: indexPath)
// Write assertions for things you expect to happen in
// awakeFromNib() method.
}
}
extension TestItemTableViewCell {
func createCell(indexPath: IndexPath) -> ItemTableViewCell {
let cell = dataSource.tableView(tableView, cellForRowAt: indexPath) as! ItemTableViewCell
XCTAssertNotNil(cell)
let view = cell.contentView
XCTAssertNotNil(view)
return cell
}
}
private class TableViewDataSource: NSObject, UITableViewDataSource {
var items = [Item]()
override init() {
super.init()
// Initialize model, i.e. create&add object in items.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell",
for: indexPath)
return cell
}
}
private class TableViewDelegate: NSObject, UITableViewDelegate {
}
This approach mimics the way UITableViewCells are created/reused at runtime. The same methods get called, e.g. awakeFromNib, IBOutlets initialized, etc. I am sure you can even test the sizing of the cell (e.g. height) even though I haven't tried that yet. Note that having a view model where the "visualization" logic of your model object is contained is a good & modular approach and makes it easier to unit test parts of the code (as described in another answer above). However, with a unit test for a view model object, you cannot test the entire lifecycle of a UITableViewCell.
Performing unit tests against that is not really worth the hassle. However, there is an easier approach to this problem.
You can create a view model to support your cell, and then test that the view model is providing the correct values for each item.
A simple example of a view model that populates two labels and an image is here:
class MyCellModel {
var stringOne: String? {
return "Compute string 1"
}
var stringTwo: String? {
return "Compute string 2"
}
var image: UIImage? {
return UIImage(named: "myimage")
}
}
Using this model, you would place the logic for generating those values in the relevant computed properties. Then for testing purposes, you can initialize this model with the values you want to test against.

Resources