unit test custom UITableViewCell? - ios

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.

Related

Unit Test for TableView isSelected Scenario Swift

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

sort tableViewCells by numbers inside a label

I try to sort the tableViewCells by numbers inside a label, so the cell which includes the highest number in a label should be last, and vice versa.
I tried it with different solutions like following, but it's simply not working, it also doesn't show any error code
I don't know if there is just a small mistake or if it is all completely wrong, but if so, I hope that you know a completely different way to solve it.
TableView:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// download jobs
jobsRef.observe(.value, with: { (snapshot) in
self.jobs.removeAll()
for child in snapshot.children {
let childSnapshot = child as! DataSnapshot
let job = Job(snapshot: childSnapshot)
print(job)
self.jobs.insert(job, at: 0)
}
filterLocation()
self.tableView.reloadData()
})
}
var jobArr = JobTableViewCell.jobDistance!.jobArr
func filterLocation() {
jobArr.sort() { $0.distance.text > $1.distance.text}
}
TableViewCell:
#IBOutlet weak var distance: UILabel!
static var jobDistance: JobTableViewCell?
var jobArr = [JobTableViewCell.jobDistance!.distance.text]
override func layoutSubviews() {
super.layoutSubviews()
JobTableViewCell.jobDistance = self
}
lets check out apple doc for the table view https://developer.apple.com/documentation/uikit/uitableviewdatasource
as it says there is method:
func tableView(UITableView, cellForRowAt: IndexPath) -> UITableViewCell
we can read it like "give me[UITableView] cell[-> UITableViewCell] for this index[cellForRowAt]"
so all we need is just map our data source to tableview indexes:
e.g.
we have datasource array of strings
var dataSource = ["String", "Very long string", "Str"]
sort...
> ["Str", "String", "Very long string"]
and then just provide our data to cell (your tableview must conform UITableViewDataSource protocol)
// Provide a cell object for each row.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Fetch a cell of the appropriate type.
let cell = tableView.dequeueReusableCell(withIdentifier: "cellTypeIdentifier", for: indexPath)
// Configure the cell’s contents.
cell.textLabel!.text = dataSource[indexPath]
return cell
}
The problem is you sort another array jobArr
jobArr.sort() { $0.distance.text > $1.distance.text}
and append values to another one jobs

Linking multiple custom UITableViewCell's to single xib

Question
I'm creating a re-usable custom UITableViewCell and then subclassing it for re-use. How do I set these various subclasses to link to the same xib file?
Background
Let's call my custom UITableViewCell PickerTableViewCell. This cell includes a UIPickerView, as well as all the implementations as to how the picker view looks and behaves. When I want to use this cell, the only thing I need to give it is the data for the picker view. So I subclass PickerTableViewCell, then simply create the data source I need and assign it to the picker view. So far this has all worked well.
Here are the relevant parts of PickerTableViewCell:
class PickerTableViewCell: UITableViewCell {
var picker: UIPickerView! = UIPickerView()
var pickerDataSource: PickerViewDataSource!
override func awakeFromNib() {
super.awakeFromNib()
self.picker = UIPickerView(frame: CGRect(x: 0, y: 40, width: 0, height: 0))
self.picker.delegate = self
self.assignPickerDataSource()
}
// Must be overriden by child classes
func assignPickerDataSource() {
fatalError("Must Override")
}
Here is an example of a subclass:
class LocationPickerTableViewCell: PickerTableViewCell {
override func assignPickerDataSource() {
self.pickerDataSource = LocationPickerDataSource()
self.picker.dataSource = self.pickerDataSource
}
}
Problem
Since I am using these cells all over the place, with different data sources, I created a xib file which defines how the cell looks called PickerTableViewCell.xib, and assign it to the class PickerTableViewCell. In the view controllers I want to use it for, I register the cell with the table view inside viewDidLoad(). Then, inside func tableView(_:, cellForRowAt) I dequeue the subclass I want like this:
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! LocationPickerTableViewCell
return cell
This is where the problem happens. The cell that is created is a PickerTableViewCell, not its subclass LocationPickerTableViewCell. This runs into the fatal error I placed in the parent class which is overriden by the child class.
The only way I have found to solve this is to create a separate xib file for each subclass I want to create, and assign it to the relevant subclass. While this solution does work, it feels wrong to have all of these xib files which are practically identical (except for which class they are assigned to) inside my project.
Is there a way I can overcome this problem, and have all of these cells link to the same single xib file?
Thanks! :)
Add view loaded by xib to UITableViewCell classes in which you want to use it.
Create your xib as per your require design, in your example PickerTableViewCell.xib
Create UITableViewCell sub-classes in which you want to use that view. I am using FirstTableViewCell & SecondTableViewCell for this.
in constructor of table cell load the xib and add it to table cell.
let nib = Bundle.main.loadNibNamed("PickerTableViewCell", owner: nil, options: nil)
if let view = nib?.first as? UIView{
self.addSubview(view)
}
if xib have any #IBOutlet then get them by viewWithTag function and assign to class variables
if let label = self.viewWithTag(1) as? UILabel{
self.label = label
}
override reuseIdentifier var of each tableviewCell subclass with different name
override var reuseIdentifier: String?{
return "FirstTableViewCell"
}
Now You can use these classes where you want, for using this follow below steps:
register this tableviewCell subclass with xib with tableview:
tableView.register(FirstTableViewCell.self, forCellReuseIdentifier:"PickerTableViewCell")
now in cellForRowAt indexPath method use it.
var cell = tableView.dequeueReusableCell(withIdentifier: "FirstTableViewCell", for: indexPath) as? FirstTableViewCell
if cell == nil {
cell = FirstTableViewCell()
}
cell?.label?.text = "FirstTableViewCell"
Don't use subclassing to assign different data sources.
Approach 1: Assign pickerDataSource in tableView(_:cellForRowAt:)
In the table view controller, you need to assign pickerDataSource
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! PickerTableViewCell
cell.pickerDataSource = LocationPickerDataSource()
return cell
}
Handle additional work needed after the assignment of pickerDataSource with a didSet.
class PickerTableViewCell: UITableViewCell {
var picker: UIPickerView! = UIPickerView()
var pickerDataSource: PickerViewDataSource! {
didSet {
self.picker.dataSource = self.pickerDataSource
}
}
…
}
Approach 2: Extend PickerTableViewCell in all the needed ways.
Here instead of subclassing add the needed logic to a uniquely named setup method each defined in their own extension.
extension PickerTableViewCell {
func setupLocationPickerDataSource() {
self.pickerDataSource = LocationPickerDataSource()
self.picker.dataSource = self.pickerDataSource
}
}
then in tableView(_:cellForRowAt:)
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier") as! PickerTableViewCell
cell.setupLocationPickerDataSource()
return cell
}

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

I am facing a problem with creation of dynamic view in Swift. However, the problem is not directly related to Swift itself, it is rather a Object-Oriented programming problem.
The problem is that I need to be able to add additional view elements to a view dynamically. And I am not sure if I'm doing it correctly. My solutions seems as overkill to me.
To solve the problem I thought Decorator pattern would be a good candidate. Additionally to have more control of the flow, I have introduced Template Method pattern.
I have a number of classes that define default look and feel on certain view controls like Labels, TextFields and Buttons. Here below you can see an approximate idea of how it is.
Here is my code:
class ViewElement{
// this class inherits from default UIKit elemnts and provides default UI view
}
// default cell is the cell that implements default elements layout and margings, etc
class DefaultCell: UITableViewCell {
let mainStack: UIViewStack
func addElement(ViewElement)
}
class BlueCell: DefaultCell {
let textField1: TextField
let label : Label
let button: Button
init(){
textField = TextField()
label = Label()
button = Button()
addElement(textField)
addElement(label)
addElement(button)
}
}
Here is the tableViewDataSource implementation
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
return cell
}
}
As you can see, the greater the number of conditions, the bigger my switch statement.
Additional problem arises from the fact that I will need to access the additional elements that I assign in the condition, like callbacks, tap events etc. Also the fact that those elements in conditional are added via addElement method, means that those elements will be added at the bottom of the mainStack.
In order to have control over the way elements are added to the stack I decided to go with the following solution: Template Method pattern
protocol OrderableElements {
func top()
func middle()
func bottom()
}
extension OrderableElements {
func render() {
top()
middle()
bottom()
}
}
Now the BlueCell implements the protocol and looks like this
class BlueCell: DefaultCell, OrderableElements {
init(){
textField = TextField()
label = Label()
button = Button()
}
func top() {
addElement(textField)
}
func middle() {
addElement(label)
}
func bottom(){
addElement(button)
}
}
The tabledatasource class will then look as follows:
class BlueTable: UITableViewDataSource {
...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
switch weather {
case good:
labelOne
labelTwo
buttonOne
cell.addElement(labelOne)
cell.addElement(labelTwo)
cell.addElement(buttonOne)
case bad:
// idem
cell.addView(badWeatherView)
}
...
**cell.render()**
return cell
}
}
Now because I need to add the new view elements in certain location or better said, at certain moments in during the scope of BlueCell, I introduced Decorators for the cell, like this:
class CellDecorator: OrderableElements {
var cell: BlueCell
init(cell: BlueCell){
self.cell = cell
}
func top() {
self.cell.top()
}
func middle(){
self.cell.middle()
}
func bottom(){
self.cell.bottom()
}
func getCell() {
return self.cell
}
}
Here is the concrete implementation
class GoodWeatherDecorator: CellDecorator {
let goodLabel
let goodTextField
let goodButton
override top() {
super.top()
addElement(goodLabel)
}
override middle(){
super.middle()
addElement(goodTextfield)
}
override bottom(){
super.bottom()
addElement(goodButton)
}
}
The final implementation of the cellForRowAt method looks like below:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var cell: = dequeue the cell
if cell == nil {
cell = BlueCell(with everything I want to pass to the constructor)
}
// then I check for the condition
var decoratedCell = CellDecorator(cell: cell)
switch weather {
case good:
decoratedCell = GoodWeatherDecorator(cell: cell)
case bad:
decoratedCell = BadWeatherDecorator(cell: cell)
}
decoratedCell.configure() // <------------ here is the configure call
cell = decoratedCell.getCell() // <------- here I get cell from the decorator
return cell
}
}
Now I do understand that my implementation of the decorator pattern is not 100% valid, because I don't inherit from the BlueCell class, for example. But that does not bother me that much. The things that bothers me is that I think that this solution to the problem is kind of overkill.
All works the right way, but I can help the feeling of having done too much to solve this trivial problem.
What do you think? How would you solve this kind of problem?
Thanks in advace
Given that you only show two types of cells and your solution doesn't actually get rid of the switch statement, I'd say that your solution counts as "overkill."
You don't show it, but it seems that you have a Weather enum. I'll assume that...
enum Weather: String {
case good
case bad
}
In the table view datasource, my goal would be to have something like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let weather = weathers[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: weather.rawValue, for: indexPath) as! ConfigurableCell
cell.configure(with: weather)
return cell as! UITableViewCell
}
In order to achieve the above, I would have several cells laid out in the storyboard file with different identifiers. I would have a subclass for each type of cell in my code where all of them conform to the ConfigurableCell protocol.
protocol ConfigurableCell {
func configure(with weather: Weather)
}
If you can't conform your Weather enum to the String type, you will need a switch statement to convert a weather object to a string identifier, but otherwise, no switch statements necessary.
You should follow Daniel T.'s answer.
But here's a suggested upgrade that I use on my own projects.
Instead of just using
protocol ConfigurableCell {
func configure(with weather: Weather)
}
I use this for reusability purposes in many different scenarios.
protocol Configurable {
associatedtype Initializables
func configure(_ model: Initializables) -> Self
}
Example use cases:
UIViewController
class SomeViewController: UIViewController {
var someIntProperty: Int?
...
}
extension SomeViewController: Configurable {
struct Initializables {
let someIntProperty: Int?
}
func configure(_ model: SomeViewController.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
return self
}
}
// on some other part of the code.
let someViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as! SomeViewController
_ = someViewController.configure(SomeViewController.Initializables(someIntProperty: 100))
UITableViewCell
class SomeTableViewCell: UITableViewCell {
var someIntProperty: Int?
var someStringProperty: Int?
...
}
extension SomeTableViewCell: Configurable {
struct Initializables {
let someIntProperty: Int?
let someStringProperty: Int?
}
func configure(_ model: SomeTableViewCell.Initializables) -> Self {
self.someIntProperty = model.someIntProperty
self.someStringProperty = model.someStringProperty
return self
}
}
// on cellForRow
let cell = tableView.dequeueReusableCell(withIdentifier: "SomeTableViewCell", for: indexPath) as! SomeTableViewCell
return cell.configure(SomeTableViewCell.Initializables(someIntProperty: 100, someStringProperty: "SomeString"))
Notes:
As you can see it's very reusable and easy to use and implement. Downside is the code generated could be long when using configure

Content of a cell in static tableview isn't shown SWIFT 3

Here is my implementation of tableView(_:cellForRowAt:):
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let index = indexPath.section
let weekDay = WeekDays.day(at: index)
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
return UITableViewCell()
}
Here is my code for my custom table view cell:
class NotSelectedCell: UITableViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
self.backgroundColor = .red
self.textLabel?.numberOfLines = 0
self.textLabel?.textAlignment = .center;
self.textLabel?.text = "Not Available"
}
}
I've also tried initializing custom cell cell = NotSelectedCell() the result is the same. The content isn't shown. dataSource or viewDelegate aren't the problem as I'm working with UITableViewController.
Here's an image
The problem is awakeFromNIB "prepares the receiver for service after it has been loaded from an Interface Builder archive, or nib file." But you're instantiating this programmatically, so that method isn't called. You could theoretically move the code to init(style:reuseIdentifier:), make sure to call super in your implementation, and do any additional customization after that point.
But, you generally wouldn't programmatically instantiate cells when using static cells. (It's the point of static cells, that IB takes care of everything for you.) You generally don't implement UITableViewDataSource at all when using static cells.
I would advise using dynamic table and have two cell prototypes, one with reuse identifier of "NotAvailable" and one with "Available" (or whatever identifiers you want). Then programmatically instantiate the cell with the appropriate identifier. (By the way, this also has the virtue that your cell with "NotAvailable" can be designed entirely in IB, and no code is needed, for that cell at least.) This way, the storyboard takes care of instantiating the appropriate cell.
So, here I have two cell prototypes in my dynamic table, one for "not available" and one for "available":
Then the code would look at the model to figure out which to instantiate:
// for the complicated cell where I want to show details of some window of availability, add IBOutlets for that cell's labels
class AvailableCell: UITableViewCell {
#IBOutlet weak var startLabel: UILabel!
#IBOutlet weak var stopLabel: UILabel!
#IBOutlet weak var doctorLabel: UILabel!
}
// some super simple model to represent some window of availability with a particular doctor in that office
struct Availability {
let start: String
let stop: String
let doctor: String
}
class ViewController: UITableViewController {
let days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
let available = ...
override func numberOfSections(in tableView: UITableView) -> Int {
return days.count
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return available[days[section]]?.count ?? 1
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return days[section]
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// see if there are any available windows for the given day, if not, return "not available" cell
guard let availabilities = available[days[indexPath.section]] else {
return tableView.dequeueReusableCell(withIdentifier: "NotAvailable", for: indexPath)
}
// otherwise, proceed with the more complicated "Available" cell where I have to populate various labels and the like
let cell = tableView.dequeueReusableCell(withIdentifier: "Available", for: indexPath) as! AvailableCell
let availability = availabilities[indexPath.row]
cell.startLabel.text = availability.start
cell.stopLabel.text = availability.stop
cell.doctorLabel.text = availability.doctor
return cell
}
}
And that would yield:
Now, clearly, I just whipped up a super primitive model, and didn't do any UI design in the "available" cell prototype other than inserting three labels. But it illustrates the idea: If your dynamic table has multiple unique cell designs, just implement cell prototypes for each with unique identifiers and instantiate the appropriate one. And this way, you enjoy full cell reuse, minimize how much visual design you have to do programmatically, etc.
You are not supposed to use the cellForRow:atIndexPath method when using static cells. The cells are static, so the loading flow is different. What i'd suggest is to connect the cells individually from the interface builder to your view controller.
STILL, if you want to do it this way you have to get your cells by calling "super" since that's the class who is actually generating your static cells.
UITableView with static cells without cellForRowAtIndexPath. How to set clear background?
EDIT:
I just noticed that this is wrong:
if self.availability.numberOfTimeslots(for: weekDay) == 0 {
let cell = NotSelectedCell(style: .default, reuseIdentifier: nil)
return cell
}
You have to use the "dequeueReusable" method or something. Then again, these are STATIC Cells, so you should just be linking the cells directly from the interface builder.

Resources