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

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

Related

Delete UITableView cell/row in UITableView with multiple sections using closure not working

I have a UITableView in a UIViewController that consists of dynamic multiple sections and dynamic multiple cells/rows in each section.
I have tried using actions, closures, index path. Etc
What I want to do is tap a UIButton in a custom UITableViewCell and have the row and corresponding data be deleted/removed.
Currently when I try this I can delete the first cell in a section but then when I try to delete the last one in the section I get a crash, Index out of bounds. It seems the UIButton retains the old indexPath.row
I have tried to reloadData() for the table but I haven’t been able to successfully solve this.
Using this solution from SO (UIButton action in table view cell):
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var myTableView: UITableView!
var sections: [String] = []
var items: [[String]] = []
var things: [[String]] = []
var things2: [[String]] = []
override func viewDidLoad() {
super.viewDidLoad()
things = [["A","B","C","D"],["X","Y","Z"],["J","A","A","E","K"]]
things2 = [["Q","W","E","R"], ["G","H","J"]]
items.append(contentsOf: things)
items.append(contentsOf: things2)
let secs: Int = Int.random(in: 1...items.count)
for num in 1...secs {
if num == 1 {
sections.append("My Stuff")
} else {
sections.append("Section \(num)")
}
}
}
}
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items[section].count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return sections[section]
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MyCustomTableViewCell
cell.theLabel?.text = items[indexPath.section][indexPath.row]
if indexPath.section == 0 {
cell.deleteButton.isHidden = false
cell.editButton.isHidden = false
cell.deleteButton.addAction {
// This is broken, when the action is added in the closure. It persists the indexPath as it is, not changed even with a reloadData()
self.items[indexPath.section].remove(at: indexPath.row)
self.myTableView.deleteRows(at: [indexPath], with: .fade)
}
cell.editButton.addAction {
print("I'm editing this row")
cell.backgroundColor = UIColor.magenta
}
} else {
cell.deleteButton.isHidden = true
cell.editButton.isHidden = true
}
return cell
}
}
// used from https://stackoverflow.com/questions/28894765/uibutton-action-in-table-view-cell/41374087
extension UIControl {
func addAction(for controlEvents: UIControl.Event = .primaryActionTriggered, action: #escaping () -> ()) {
let sleeve = ClosureSleeve(attachTo: self, closure: action)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
}
}
class ClosureSleeve {
let closure: () -> ()
init(attachTo: AnyObject, closure: #escaping () -> ()) {
self.closure = closure
objc_setAssociatedObject(attachTo, "[\(arc4random())]", self,.OBJC_ASSOCIATION_RETAIN)
}
#objc func invoke() {
closure()
}
}
The problem with this being used in multiple sections is that it retains the original indexPath and row.
Example: if section 0 has two items (0,1)
I delete section 0 item 0 - it works as expected.
Then when I tap the button to delete the remaining cell, which was section 0 item 1 and should have been reloaded to section 0 item 0. It fails because the closure still sees the code block as being section 0 item 1. Even through a reloadData() the closure still sees the original assignment of indexPath.section and indexPath.row
Anyone have any ideas?
I'm thinking it has something to do with the retain, but honestly, I'm drawing a blank. I've looked at this code in a debugger for a day or two and I'm frustrated and need some Yoda help...

How insert multiple custom cells (one for each xib file) in multiple sections in a TableviewController in Swift?

struct Objects{
var section: String! //Section name
var cellIndex: [String] //cell index
}
var objects = [Objects]()
override func viewDidLoad() {
super.viewDidLoad()
objects = [Objects(section: "Profile", cellIndex: ["name", "gender"]), Objects(section: "Change Login", cellIndex: ["changeLogin"])]
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return objects.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return objects[section].cellIndex.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell {
if objects[indexPath.row].cellIndex == "name" {
let cell = tableView.dequeueReusableCellWithIdentifier("profileNameCell") as!ProfileNameCell
return cell
}else
if objects[indexPath.row].cellIndex == "gender" {
let cell = tableView.dequeueReusableCellWithIdentifier("profileGenderCell") as!ProfileGenderCell
return cell
}else{
objects[indexPath.row].cellIndex == "changeLogin" {
let cell = tableView.dequeueReusableCellWithIdentifier("profileChangeLoginCell") as!ProfileChangeLoginCell
return cell
}
Each "ProfileNameCell", "ProfileNameCell", "ProfileChangeLoginCell" is connected to a xib file.
The code above don't compile.
I read code examples and I tried to go further on my code, but I don't know how insert the sections.
Advices? Thank you very much.
EDIT:
I decided to create the cells directly in the table view, like Vadian said, and it worked.
I had tried it before but the cells components had not connected to UiTableViewController. Because of this problem I had decided using xib files. Now the elements are connecting. To show all the cells and sections I used the code below:
struct Cells{
var section: String!
var rows: [String]!
}
var tableStructure: [Cells] = []
override func viewDidLoad() {
super.viewDidLoad(
tableStructure = [Cells(section: "One", rows: ["1", "2", "3", "4", "5"]), Cells(section: "Two", rows: ["1"]), Cells(section: "Three", rows: ["1", "2"])]
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return tableStructure.count
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tableStructure[section].rows.count
}
var cellIndex: String //cell index
objects = [Objects(section: "Profile", cellIndex: ["name"]),Objects(section: "Profile", cellIndex: ["gender"]), Objects(section: "Change Login", cellIndex: ["changeLogin"])]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return objects.count
}
try this one if you want 3 custom cells for your tableview
Every Section contains Rows, so when the UITableViewDelegate asks in cellforRowAtIndexPath, the following structure is needed:
Section 1
Row1
Row2
Row3
Section 2
Row1
Row2
...
So every Section contains some rows, and every row / section could have a different cell layout / class.
So you dont need xib Layouts for every cell (when using Storyboard) - just make some different Layouts inside your UITableView or UITableViewController and connect your IBOutlets to a specific UITableViewCell class.
Now you should have the following structure, as described above.
let cellObjects = [ClassForSection: [ClassForRow]]()
Then you could use:
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return cellObjects.count
}
And:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath:NSIndexPath) -> UITableViewCell {
let currentRowItem = cellObjects[indexPath.section][indexPath.row]
// so you have an specific object for every cell
}

Xcode UI test : Accessibility query fail on UITableViewCell

The issue
Using Xcode UI test, I can not query the cells in a UITableView
Explanations
The UITableView
The UITableView contains 3 cells :
import UIKit
#objc class DumpTable: UITableViewController {
var objects: [NSDate] = [NSDate]()
override func viewDidLoad() {
super.viewDidLoad()
objects.append(NSDate())
objects.append(NSDate())
objects.append(NSDate())
tableView.isAccessibilityElement = true
tableView.accessibilityLabel = "Thetable"
tableView.accessibilityIdentifier = "Thetable"
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let object = objects[indexPath.row]
cell.textLabel!.text = object.description
cell.isAccessibilityElement = true
cell.accessibilityLabel = "Thecell"
cell.accessibilityIdentifier = "Thecell"
return cell
}
}
The test
The test is really simple.
Given a UITableView with 3 cells, I'm trying to assert there are any cells available :
XCTAssertTrue(XCUIApplication().tables["Thetable"].exists)
XCTAssertTrue(XCUIApplication().tables["Thetable"].cells.count > 0)
It will then fail on the 2 assertions :
Assertion Failure: XCTAssertTrue failed -
/Users/damiengavard/Desktop/Table/TableUITests/TableUITests.swift:33: error: -[TableUITests.TableUITests testExample] : XCTAssertTrue failed -
How to reproduce
https://github.com/dagio/TableCellAccessibility
Simply execute Cmd+U
I found the answer here. In order to make the UITableViewCell accessible, the containing UITableView cannot be accessible itself.
So, you just need to remove these lines:
tableView.isAccessibilityElement = true
tableView.accessibilityLabel = "Thetable"
tableView.accessibilityIdentifier = "Thetable"
In your example project, you are looking for a table within a table.
let tableView = XCUIApplication().tables.containingType(.Table, identifier: "Thetable")
You should use matchingIdentifier:, which searches through the tables on the screen, instead of containingType:identifier:, which searches through the descendants of the tables on the screen.

Mutli-threaded giving results out of order

I'm really new to multithreading. It has produced a result I wasn't expecting, but it makes sense when I think about it. Now I need to find a way to make it work right.
Here is the scenario....
I have routine that calls a web page with a requested date. Let's call it getPage(inDate: String). The results are of unknown, but different sizes, depending on the date submitted. (Today could have 100 items, tomorrow 2 items, the next day 10 items).
I have a loop that calls getPage, in date sequence.
Each return is a section in a tableview.
PSEUDO CODE
func viewDidLoad() {
...
for cntr in 1...4 {
getPage(calculatedDate)
}
...
}
getPage(inDate: String) {
let task = NSURLSession ( ....) {
...
// create tableview section data using self. arrays of string
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
self.tableView.reloadData()
})
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return self.dateSection.count // array of sections
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataItem1[section].count // an array of data for the section
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let mycell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! calCell
self.navigationItem.title = "Calendar Data"
// Configure the cell...
mycell.dataItem1.text = self.data1[indexPath.section][indexPath.row]
mycell.dataItem2.text = self.data2[indexPath.section][indexPath.row]
mycell.dataItem3.text = self.data3[indexPath.section][indexPath.row]
mycell.dataItem4.text = self.data4[indexPath.section][indexPath.row]
return mycell
}
override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
// print("In titleForHederInSection: Section \(section)")
// print("Title: \(self.dateSection[section])")
return self.dateSection[section]
}
It doesn't seem to matter where I put the reload(), or if I use dispatch_sync or dispatch_async, the table is out of order...giving me the quickest returns first.
My initial attempts with semaphores has been a dismal failure...locking up the application forever.
So the question is, how do I get the viewtable to be built in the order that I called the web page (in this case, date order)?

swift 2 how to remove empty rows in a tableview in a viewcontroller

Is there a way to reset tableview height so that no empty rows are showed. For an example, a tableview displays 3 rows but there is only one row having real data. I'd like the tableview shrinks it size so there is only one row display
I guess you have a View Controller like this
class Controller: UITableViewController {
private var data: [String?] = ["One", nil, "Three", nil, nil]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellID")!
cell.textLabel?.text = data[indexPath.row]
return cell
}
}
Since your model (data in my example) contains nil values you are getting a table view like this
One
_
Three
_
_
Removing the empty rows
Now you want instead a table view like this right?
One
Three
Filtering your model
Since the UI (the table view) is just a representation of your model you need to change your model.
It's pretty easy
class Controller: UITableViewController {
private var data: [String?] = ["One", nil, "Three", nil, nil]
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellID")!
cell.textLabel?.text = data[indexPath.row]
return cell
}
override func viewDidLoad() {
data = data.filter { $0 != nil }
super.viewDidLoad()
}
}
As you can see, inside viewDidLoad() I removed the bill values from the data array. Now you'll get only cells for real values.
I think you need remove the empty data before execute func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int.
//EDITED
I give you this possible solution, maybe there are more ways to do it, you can try with this.
class MyViewController: UITableViewDataSource, UITableViewDelegate {
private var realHeight = 0
private var data : [String] = []
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellID")!
cell.textLabel?.text = data[indexPath.row]
self.realHeight += self.tableView.rowHeight //Increase real height value.
return cell
}
func loadData() { //This function reload the data
self.realHeight = 0
self.tableView.reloadData()
//Update tableViewHeight here, use self.realHeight. You can use constraints or update directly the frame.
}
}
The solution is have a counter that represents the sum of all visibles rows height. e.g If you have n cells then your height will be self.tableView.rowHeight * n, then you ever will have an effective height.
I recommend you to create a constraint for the table height and when the cells change, you only need change the constant of the constraint, is more easy than change the frame.