Get number rows in UITableView while unit tests- swift? - swift

I'm writing a test case for UIViewController that has UITableView. I want to ask how can I get number of rows in UITableView
func testloadingDataIntoUiTableView()
{
var countRow:Int = viewController.formListTableView.numberOfRowsInSection
XCTAssert(countRow == 4)
}

Introduction
Please keep in mind that the data model generates the UI. But you should not query the UI to retrieve your data model (unless we are talking about user input).
Lets look at this example
class Controller:UITableViewController {
let animals = ["Tiger", "Leopard", "Snow Leopard", "Lion", "Mountain Lion"]
let places = ["Maveriks", "Yosemite", "El Capitan"];
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return animals.count
case 1: return places.count
default: fatalError()
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCellWithIdentifier("MyCellID") else { fatalError("Who took the MyCellID cell???") }
switch indexPath.section {
case 0: cell.textLabel?.text = animals[indexPath.row]
case 1: cell.textLabel?.text = places[indexPath.row]
default: fatalError()
}
return cell
}
}
The ugly solution
In this case to get the total number of rows into the table we should query the model (the animals and places properties), so
let controller: Controller = ...
let rows = controller.animals.count + controller.places.count
The nice solution
Or even better we could make the animals and places properties private and add a computed property like this
class Controller:UITableViewController {
private let animals = ["Tiger", "Leopard", "Snow Leopard", "Lion", "Mountain Lion"]
private let places = ["Maveriks", "Yosemite", "El Capitan"];
var totalNumberOfRows: Int { return animals.count + places.count }
...
Now you can use this
let controller: Controller = ...
let rows = controller.totalNumberOfRows

Related

Segmented Control with a UITableView

I have a segmented control that is filled using an enum. I have a table view to show the data of each case. What is the proper way to handle this use-case rather than hardcoding switch-cases for numberOfRowsInSection and cellForRowAt?
let segmentedControl = UISegmentedControl(items: SegmentedControlItems.allCases.map {$0.rawValue})
private enum SegmentedControlItems: String, CaseIterable {
case case1 = "Case1"
case case2 = "Case2"
case case3 = "Case3"
}
private var arr1 = [Type1]()
private var arr2 = [Type2]()
private var arr3 = [Type3]()
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch view?.segmentedControl.selectedSegmentIndex {
case 0:
return case1.count
case 1:
return case2.count
case 2:
return case3.count
default:
return 0
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: AssetView.AssetCell.reuseIdentifier, for: indexPath) as? AssetView.AssetCell else {
fatalError("Error trying to dequeue cell")
}
switch view?.segmentedControl.selectedSegmentIndex {
case 0:
setupCell(case1)
case 1:
setupCell(case2)
case 2:
setupCell(case3)
default:
return UITableViewCell()
}
return cell
}
put case1, case2, and case3 in an array. Let's call it cases:
let cases = [case1, case2, case3]
Then index into that using your segmented control's index:
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let selectedItem = view?.segmentedControl.selectedSegmentIndex,
selectedItem < cases.count else { fatalError("splat!" }
return cases[selectedItem]
}
Perfect article about how to use table view with different datasources. It really helped me.
https://betterprogramming.pub/uitableview-sections-with-nested-types-27eabb833ad9

Alphabetic sequencing in TableView of iOS Swift 4

The original data is in JSON, which is downloaded and packed in to a Model Class called Article.swift. "article" is its element. We have
article.name = rawData["A_Name_Ch"] as! String
article.name_EN = rawData["A_Name_En"] as! String
article.location = rawData["A_Location"] as! String
article.image_URLString = rawData["A_Pic01_URL"] as? String
........
........
When showing the data on a tableviewController as below, data is sequenced by data's original sequence in JSON. It is sequenced by a key "ID". But, on Swift 4, how to sort it by AlphaBetic sequence referring to the key "article.name_EN"(in English)?
// MARK: - Table view data source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if(searchActive) {
return filtered.count
}
return articles.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ListTableCell", for: indexPath) as! ListTableCell
var article : Article
if(searchActive){ article = filtered[indexPath.row] }
else{ article = articles[indexPath.row] }
let imageURL: URL?
if let imageURLString = article.image_URLString {
imageURL = URL (string: imageURLString)
}
else { imageURL = nil }
if let c = cell as? ListTableCell {
c.nameLabel?.text = article.name;
c.name_ENLabel?.text = article.name_EN;
c.locationLabel?.text = article.location
}
}
return cell
}
You need to sort your object of the array by property name.
articles.sorted(by: { $0.name_EN > $1.name_EN })
For Asc:
articles.sorted(by: { $0.name_EN < $1.name_EN })
You have both filtered array and original array. Apply the sort on both arrays before populating to the tableView.

How can I use an enum in cellForRowAtIndexPath to populate tableView cells?

I have an enum with several cases that I'm using for calculations and the user will be able to set one as their preference. They of course need to be able to change that preference so I want to show these in a table view so they can see them all and choose the one they want to set as their preference.
enum Calculation: Int {
case Calculation1
case Calculation2
case Calculation3
case Calculation4
case Calculation5
case Calculation6
case NoChoice // this exist only for the little trick in the refresh() method for which I need to know the number of cases
// This static method is from http://stackoverflow.com/questions/27094878/how-do-i-get-the-count-of-a-swift-enum/32063267#32063267
static let count: Int = {
var max: Int = 0
while let _ = Calculation(rawValue: max) { max += 1 }
return max
}()
static var selectedChoice: String {
get {
let userDefaults = NSUserDefaults.standardUserDefaults().objectForKey("selectedCalculation")
if let returnValue = userDefaults!.objectForKey("selectedCalculation") as? String {
return returnValue // if one exists, return it
} else {
return "Calculation1" // if not, return this default value
}
}
set {
NSUserDefaults.standardUserDefaults().setObject(newValue, forKey: "selectedCalculation")
NSUserDefaults.standardUserDefaults().synchronize()
}
}
}
The problem is that an enum doesn't have an indexPath so I can't iterate through it and grab those case names:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("formulaCell", forIndexPath: indexPath)
// Configure the cell...
let currentFormula = Calculation[indexPath.row] <- Error: Type 'Calculation.Type' has no subscript members
cell.textLabel?.text = currentFormula
return cell
}
The best I could come up with is to create an array of those cases and use it to create the cells:
let Calculation = [Calculation1, Calculation2, Calculation3 ...etc]
which worked but is clearly an ugly hack.
Is there a better way to do this?
use a switch statement on your enum to handle creating cell for each case.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("formulaCell", forIndexPath: indexPath)
let calculation = Calculation(rawValue: indexPath.row + 1) //Start at 1 and add one.
switch calculation {
case .Calculation1:
//Configure cell for case 1
case .Calculation2
//Configure cell for case 2 etc...
default:
}
return cell
}

Swift, "subclass" the data source methods of UITableView somehow?

Imagine a table view controller ExtraRowTableViewController,
which always inserts an extra row, after (let's say) the third row.
So in this example ...
class SomeList:ExtraRowTableViewController
override func numberOfSectionsInTableView(tableView: UITableView)->Int
{
return yourData.count ... say, 50 items
}
override func tableView
(tableView:UITableView, cellForRowAtIndexPath indexPath:NSIndexPath)
-> UITableViewCell
{
return yourData.cell ... for that row number
}
ExtraRowTableViewController would "take over" and actually return 51.
For cellForRowAtIndexPath, it would "take over" and return its own cell at row four, it would return your cell row N from 0 to 3, and it would return your cell row minus one for rows above four.
How can this be achieved in ExtraRowTableViewController ?
So that the programmer of SomeList need make no change at all.
Would you be subclassing UITableView, or the data source delegate .. or??
To clarify, an example use case might be, let's say, adding an ad, editing field, or some special news, at the fourth row. It would be appropriate that the programmer of SomeList need do absolutely nothing to achieve this, ie it is achieved in a completely OO manner.
Note that it's, of course, easy to just add new "substitute" calls, which your table view would "just know" to use instead of the normal calls. (RMenke has provide a useful full example of this below.) So,
class SpecialTableViewController:UITableViewController
func tableView(tableView: UITableView, specialNumberOfRowsInSection section: Int) -> Int
{
print ("You forgot to supply an override for specialNumberOfRowsInSection")
}
func tableView
(tableView:UITableView, specialCellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell
{
print ("You forgot to supply an override for specialCellForRowAtIndexPath")
}
override final func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return self.specialNumberOfRowsInSection(section) + 1
}
override final func tableView
(tableView:UITableView, cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell
{
if indexPath.row == 4
{ return ... the special advertisement cell ... }
if indexPath.row < 4
{ return self.specialCellForRowAtIndexPath( indexPath )
if indexPath.row > 4
{ return self.specialCellForRowAtIndexPath( [indexPath.row - 1] )
}
In the example your table view programmer would have to "just know" that they must use specialNumberOfRowsInSection and specialCellForRowAtIndexPath in SpecialTableViewController rather than the usual calls ... it's not a clean, drop-in, OO solution.
Note: I appreciate you could probably subclass NSObject in some way to override the signals (such as discussed here), but that is not a language solution.
github link -> might contain more updated code
To answer the question: It is not possible to override the standard flow of the functions between the UITableViewController and the UITableViewDataSource in the form of a subclass.
The UIKit source code is like a big black box which we can not see or alter. (apps will be rejected if you do.) To do exactly what you want you would need to override the functions that call on the functions from the UITableViewDataSource so they point to a third function instead of to the protocol functions. This third function would alter the basic behaviour and trigger the function from the UITableViewDataSource. This way it would all stay the same for other devs.
Hack : Subclass the entire UITableviewController -> you need stored properties. This way other people can subclass your custom class and they won't see any of the magic/mess under the hood.
The class below uses the same style as the regular UITableViewController. Users override the methods they wish to alter. Because those methods are used inside the existing function you get an altered functionality.
Unfortunately it is not possible to mark those functions as private.
The adapter for the indexPath stores a Bool and the original indexPath. -> This will correspond to your data.
The new inserted cells will get an indexPath based on the section they are created in and a counter. -> Could be useful.
Update: Add x extra rows after y rows
class IATableViewController: UITableViewController {
private var adapters : [[cellAdapter]] = []
private struct cellAdapter {
var isDataCell : Bool = true
var indexPath : NSIndexPath = NSIndexPath()
}
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func cellIdentifier(tableView: UITableView, isDataCell: Bool) -> String {
return "Cell"
}
func numberOfSections(tableView: UITableView) -> Int {
return 0
}
func numberOfRowsInSection(tableView: UITableView, section: Int) -> Int {
return 0
}
func insertXRowsEveryYRows(tableView: UITableView, section: Int) -> (numberOfRows:Int, everyYRows:Int)? {
//(numberOfRows:0, everyYRows:0)
return nil
}
func insertXRowsAfterYRows(tableView: UITableView, section: Int) -> (numberOfRows:Int, afterYRows:Int)? {
//(numberOfRows:0, afterYRows:0)
return nil
}
internal override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// #warning Incomplete implementation, return the number of sections
let sections = numberOfSections(tableView)
adapters = []
for _ in 0..<sections {
adapters.append([])
}
return sections
}
internal override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// #warning Incomplete implementation, return the number of rows
let rows = numberOfRowsInSection(tableView, section: section)
adapters[section] = []
for row in 0..<rows {
var adapter = cellAdapter()
adapter.indexPath = NSIndexPath(forRow: row, inSection: section)
adapter.isDataCell = true
adapters[section].append(adapter)
}
insertion(tableView, section: section)
return adapters[section].count
}
private func insertion(tableView: UITableView, section: Int) {
if let insertRowEvery = insertXRowsEveryYRows(tableView, section: section) {
let insertionPoint = insertRowEvery.everyYRows
let insertionTimes = insertRowEvery.numberOfRows
var counter = 0
var startArray = adapters[section]
var insertionArray: [cellAdapter] = []
while !startArray.isEmpty {
if startArray.count > (insertionPoint - 1) {
for _ in 0..<insertionPoint {
insertionArray.append(startArray.removeFirst())
}
for _ in 0..<insertionTimes {
var adapter = cellAdapter()
adapter.indexPath = NSIndexPath(forRow: counter, inSection: section)
adapter.isDataCell = false
insertionArray.append(adapter)
counter += 1
}
} else {
insertionArray += startArray
startArray = []
}
}
adapters[section] = insertionArray
}
else if let insertRowAfter = insertXRowsAfterYRows(tableView, section: section) {
let insertionPoint = insertRowAfter.afterYRows
let insertionTimes = insertRowAfter.numberOfRows
if adapters[section].count > (insertionPoint - 1) {
for i in 0..<insertionTimes {
var adapter = cellAdapter()
adapter.indexPath = NSIndexPath(forRow: i, inSection: section)
adapter.isDataCell = false
adapters[section].insert(adapter, atIndex: insertionPoint)
}
}
}
}
func insertionCellForRowAtIndexPath(tableView: UITableView, cell: UITableViewCell, indexPath: NSIndexPath) -> UITableViewCell {
return cell
}
func dataCellForRowAtIndexPath(tableView: UITableView, cell: UITableViewCell, indexPath: NSIndexPath) -> UITableViewCell {
return cell
}
internal override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let adapter = adapters[indexPath.section][indexPath.row]
let identifier = cellIdentifier(tableView, isDataCell: adapter.isDataCell)
let cell = tableView.dequeueReusableCellWithIdentifier(identifier, forIndexPath: indexPath)
switch adapter.isDataCell {
case true :
return dataCellForRowAtIndexPath(tableView, cell: cell, indexPath: adapter.indexPath)
case false :
return insertionCellForRowAtIndexPath(tableView, cell: cell, indexPath: adapter.indexPath)
}
}
}

Table View UI error: Swift

On the last question I asked about the code error in my animal table view project, now I finished the initial coding, but my UI turned really strange. It is missing the first letter of each animal name and the table view prototype cell.
For example, amel should be camel and hinoceros should be rhinoceros.
Is this a bug from the code here?
import UIKit
class AnimalTableViewController: UITableViewController {
var animalsDict = [String: [String]] ()
var animalSelectionTitles = [String] ()
let animals = ["Bear", "Black Swan", "Buffalo", "Camel", "Cockatoo", "Dog", "Donkey", "Emu", "Giraffe", "Greater Rhea", "Hippopotamus", "Horse", "Koala", "Lion", "Llama", "Manatus", "Meerkat", "Panda", "Peacock", "Pig", "Platypus", "Polar Bear", "Rhinoceros", "Seagull", "Tasmania Devil", "Whale", "Whale Shark", "Wombat"]
func createAnimalDict() {
for animal in animals {
let animalKey = animal.substringFromIndex(advance(animal.startIndex, 1))
if var animalValues = animalsDict[animalKey] {
animalValues.append(animal)
animalsDict[animalKey] = animalValues
} else {
animalsDict[animalKey] = [animal]
}
}
animalSelectionTitles = [String] (animalsDict.keys)
animalSelectionTitles.sort({ $0 < $1})
animalSelectionTitles.sort( { (s1:String, s2:String) -> Bool in
return s1 < s2
})
}
override func viewDidLoad() {
super.viewDidLoad()
createAnimalDict()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Table view data source
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
// Return the number of sections.
return animalSelectionTitles.count
}
override func tableView(tableView: UITableView, titleForHeaderInSection section:Int) -> String? {
// Return the number of rows in the section.
return animalSelectionTitles[section]
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! UITableViewCell
let animalKey = animalSelectionTitles[indexPath.section]
if let animalValues = animalsDict[animalKey] {
cell.textLabel?.text = animalValues[indexPath.row]
let imageFileName = animalValues[indexPath.row].lowercaseString.stringByReplacingOccurrencesOfString("", withString: "_", options: nil, range: nil)
cell.imageView?.image = UIImage(named:imageFileName)
}
return cell
}
}
So far I can say the error is in your createAnimalDict() method. In the line
let animalKey = animal.substringFromIndex(advance(animal.startIndex, 1))
exchange the second parameter in advance to 0 so it be:
let animalKey = animal.substringFromIndex(advance(animal.startIndex, 0))
In fact I don't really know what you are trying to do.
In this method:
override func tableView(tableView: UITableView, titleForHeaderInSection section:Int) -> String? {
// Return the number of rows in the section. (THIS COMMENT IS INCORRECT)
return animalSelectionTitles[section]
}
You are returning the title for each section. However your animalSelectionTitles[index] contains the animal name without the first letter due to its construction in createAnimalDict
Use the animal array instead for providing the full animal names:
override func tableView(tableView: UITableView, titleForHeaderInSection section:Int) -> String? {
return animals[section]
}
However note that due to the removal of the first letter you might have two animals mapping to the same key so if not necessary use the entire animal name as the key.