Create custom UITableView class - swift

How i can to create heir of UITableView class that will contain delegate and dataSource methods? I don't want to have dataSource and delegate methods in my ViewController.

You need to create another class for the purpose but first let's see how the ViewController will be.
As you can see the code is sort of self explanatory, I have created a custom class called TableViewDelegate that will be set as a delegate and dataSource of the tableView.
We are passing to TableViewDelegate, the data to be shown in the tableView, and function named didSelectRow that will be called by TableViewDelegate once a row is selected.
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
// data source
var data = [1, 2, 3, 4]
// delegate
var tableViewDelegate: TableViewDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// creating the delegate object and passing the data
tableViewDelegate = TableViewDelegate(data: data)
// passing a function to the delegate object
tableViewDelegate?.didSelectRow = didSelectRow
// setting the delegate object to tableView
tableView.delegate = tableViewDelegate
tableView.dataSource = tableViewDelegate
}
// a function that will be called by the delegate object
// when a row is selected
func didSelectRow(dataItem: Int, cell: UITableViewCell) {
let alert = UIAlertController(title: "Info", message: "\(dataItem) was selected.", preferredStyle: .Alert)
alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
presentViewController(alert, animated: true, completion: nil)
}
}
The TableViewDelegate that is in charged of everything related of UITableViewDelegate, and UITableViewDataSource protocols.
class TableViewDelegate: NSObject, UITableViewDelegate, UITableViewDataSource {
var data = [Int]()
// variable that holds a stores a function
// which return Void but accept an Int and a UITableViewCell as arguments.
var didSelectRow: ((dataItem: Int, cell: UITableViewCell) -> Void)?
init(data: [Int]) {
self.data = data
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
let text = String(data[indexPath.row])
cell.textLabel?.text = text
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.cellForRowAtIndexPath(indexPath)!
let dataItem = data[indexPath.row]
if let didSelectRow = didSelectRow {
// Calling didSelectRow that was set in ViewController.
didSelectRow(dataItem: dataItem, cell: cell)
}
}
}
Result:

I did this to avoid a long ViewController with a UIPickerViewDelegate/DS. You can simply make a class that conforms to UITableViewDelegate and UITableViewDataSource, instantiate this object in your view controller and assign it as the dataSource and delegate of the table view. For this class to send stuff back to your ViewController, you will have to make a protocol for the VC to conform to and give the class a delegate as well.
I read that this class must inherit from NSObject as the protocols are NSObject protocols, and it throws and error if they don't.
class MyCustomTableViewDel: NSObject, UITableViewDataSource, UITableViewDelegate {
weak var secondaryDelegate: TableViewSecondaryDelegate?
let rowData: [String]
init(dataForRows: [String]) {
rowData = dataForRows
super.init()
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int{
return rowData.count
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
secondaryDelegate?.doSomething(indexPath.row)
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
....
return SomeCellForTheTableView
}
}
then make the secondary protocol:
protocol TableViewSecondaryDelegate {
func doSomething(row: Int)
}
then in your ViewController:
class myTableViewSceneController: UIViewController, TableViewSecondaryDelegate {
override func viewDidLoad() {
....
let tableViewDelAndDS = MyCustomTableViewDel(dataForRows: ["row0", "row1"])
tableViewDelAndDS.secondaryDelegate = self
tableView.delegate = tableViewDelAndDS
tableView.dataSource = tableViewDelAndDS
}
func doSomething(row: Int) { ... }
}

Related

How do I call a different function for each TextField in a UITableView (Swift)?

I have a UITableView and my prototype cell consists of a label and a TextField. I also have a class MyClass that contains functions func1, func2, fun3, ... I have several ViewControllers that use the same tableViewCell prototype. Each viewController will have an instance of MyClass, called inst1, inst2, and inst3. When I enter text into FirstViewController's TableView I want each row to call a function from the instance of MyClass that corresponds to the row.
So when I enter text into row 1 on the FirstViewController I want to pass the data entered into the textField into func1 of inst1. When data is entered into row 2 of FirstViewController I want the data in the textfield to be passed into func2 of inst1. And so on and so forth down the rows.
I am very new to this and would really appreciate some help figuring out how to do this. Let me know if that doesn't make sense and I can try to rephrase it. I really need help with this. Thanks in advance!
*Updated question to show my code
Below is my Code:
FirstViewController.swift
extension FirstViewController: MyCellDelegate {
func MyCell(_ cell: UITableViewCell, didEnterText text: String) {
if let indexPath = tableView.indexPath(for: cell) {
if (indexPath.hashValue == 0) {
inst1.func1(one: text)
}
if (indexPath.hashValue == 1) {
inst1.func2(two: text)
}
}
totalText.text = inst1.getMyTotal()
}
}
import UIKit
class FirstViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
let inst1 = MyClass()
#IBOutlet weak var totalText: UILabel!
#IBOutlet weak var tableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 11
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myTableCell") as! TableViewCell
let text = cell.cellData[indexPath.row]
cell.myTextField.tag = indexPath.row
cell.delegate = self
cell.myLabel.text = text
cell.myTextField.placeholder = text
return cell
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
TableViewCell.swift
import UIKit
protocol MyCellDelegate: class {
func MyCell(_ cell: UITableViewCell, didEnterText text: String)
}
class TableViewCell: UITableViewCell {
weak var delegate: MyCellDelegate?
public var cellData: [String] = ["1","2","3","4","5","6","7","8","9","10","11"]
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var myTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
}
override func setSelected(_ selected: Bool, animated: Bool) {
}
}
When I set a breakpoint in the FirstViewController extension it never runs that code.
In WillDisplayCell add the tag to the UITextField. Also create a protocol to notify the Corrosponding viewController and set itself as the delegate here.
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier")
cell.textField.tag = indexPath.row
cell.delegate = self
}
The protocol in your cell class will look something like this
protocol MyCellDelegate: class {
func MyCell(_ cell: UITableViewCell, didEnterText text: String)
}
class MyCell: UITableViewCell, UITextFieldDelegate {
weak var delegate: MyCellDelegate?
override fun awakeFromNib() {
super.awakeFromNib()
textField.delegate = self
}
//All the remaining code goes here
func textFieldShouldReturn(_ textField: UITextField) -> Bool { //delegate method
textField.resignFirstResponder()
delegate?.MyCell(self, didEnterText: textField.text! )
return true
}
}
Now again in your FirstViewController which has conformed to be its delegate do this
extension FirstViewController: MyCellDelegate {
func MyCell(_ cell: UITableViewCell, didEnterText text: String) {
if let indexPath = tableView.indexPathForCell(cell) {
// call whichever method you want to call based on index path
}
}

Add target for button in table view cells

I have a table view that its cells have a button in themselves and these buttons should open a view with an unique id. So I need to passing an argument to my buttons but with addTarget property I just can call function without any parameter.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.editButton.addTarget(self, action: #selector(goToEdit(id:)), for: .touchUpInside)
}
func goToEdit(id: String) {
let edit = EditAdViewController(editingAdId: id)
self.navigationController?.pushViewController(edit, animated: true)
}
Is there any way to refer an action with some parameters to a button? Thanks everyone :)
You can try adding delegate functions to your custom UITableViewCell.
For example, I have a button inside this custom tableViewCell:
PickupTableViewCell.swift
import UIKit
protocol PickupTableViewCellDelegate: NSObjectProtocol {
func pickupTableViewCell(userDidTapPickup pickup: Pickup, pickupTableViewCell: PickupTableViewCell)
}
class PickupTableViewCell: UITableViewCell {
// MARK: - Properties
#IBOutlet private weak var label_UserFullName: UILabel!
....
// MARK: - Functions
// MARK: IBAction
#IBAction func pickup(_ sender: Any) {
self.delegate?.pickupTableViewCell(userDidTapPickup: self.pickup, pickupTableViewCell: self)
}
}
Then in I conform my controller via the UITableViewDataSource (cellForRow) and of course implement the delegate function of my tableViewCell.
HomeViewController.swift
// MARK: - UITableViewDataSource
extension HomeViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let pickupTVC = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.pickupTableViewCell)!
pickupTVC.delegate = self
pickupTVC.pickup = self.pickups[indexPath.section]
return pickupTVC
}
}
// MARK: - PickupTableViewCellDelegate
extension HomeViewController: PickupTableViewCellDelegate {
func pickupTableViewCell(userDidTapPickup pickup: Pickup, pickupTableViewCell: PickupTableViewCell) {
// Do something
}
}
Maybe you can try to link your button to a #IBAction and use params[indexPath.row].
To get the indexPath:
var cell = sender.superview() as? UITableViewCell
var indexPath: IndexPath? = yourTableView.indexPath(for: cell!)

Some UITableView Delegate methods called 5 times, others not called at all

I have a separate class from a UIViewController set up as my UITableView delegate and my UITableViewDataSource.
I attempt to initialize the UITableViewDelegate class and then assign it to the UITableView.
Here's what's odd...
The methods numberOfSectionsInTableView and tableView(:numberOfRowsInSection) are called five times, while tableView(_:cellForRowAtIndexPath) is never called.
I have verified both numberOfSectionsInTableView and tableView(:numberOfRowsInSection) return values of at least one.
If I move the UITableViewDataSource and UITableViewDelegate methods to the ViewController, the code works correctly.
What is causing this behavior?
class MessagesViewController: UIViewController, ManagedObjectContextSettable {
var managedObjectContext: NSManagedObjectContext!
#IBOutlet var messagesTableView: UITableView!
override func viewDidLoad() {
setupMessagesTableView()
}
private func setupMessagesTableView() {
let dataSource = MessagesTableViewDataSource(managedObjectContext: managedObjectContext, conversationList: fetchedObjects as! [Conversation])
// Assume fetchedObjects is an array fetched from CoreData store. I have removed the code that defines it for the purpose of this example.
self.messagesTableView.dataSource = dataSource
self.messagesTableView.delegate = dataSource
}
}
class MessagesTableViewDataSource: NSObject, UITableViewDataSource, UITableViewDelegate {
var managedObjectContext: NSManagedObjectContext!
var conversationList: [Conversation]
required init(managedObjectContext: NSManagedObjectContext, conversationList: [Conversation]) {
self.managedObjectContext = managedObjectContext
self.conversationList = conversationList
let conversation = Conversation()
self.conversationList.append(conversation)
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.conversationList.count }
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CellID", forIndexPath: indexPath) as UITableViewCell!
let conversation: Conversation = self.conversationList[indexPath.row]
cell.textLabel!.text = conversation.name as? String
return cell
}
}
The problem is that your instance of MessagesTableViewDataSource gets deallocated. The delegate and dataSource properties on UITableView are weak. You declare your datasource (MessagesTableViewDataSource) as a local variable inside your function and thus nothing holds a strong reference to the instance of MessagesTableViewDataSource.
To fix this, define an instance variable for dataSource and assign it in viewDidLoad.
Example Code:
class MessagesViewController: UIViewController, ManagedObjectContextSettable {
let dataSource: MessagesTableViewDataSource?
var managedObjectContext: NSManagedObjectContext!
#IBOutlet var messagesTableView: UITableView!
override func viewDidLoad() {
setupMessagesTableView()
}
private func setupMessagesTableView() {
dataSource = MessagesTableViewDataSource(managedObjectContext: managedObjectContext, conversationList: fetchedObjects as! [Conversation])
// Assume fetchedObjects is an array fetched from CoreData store. I have removed the code that defines it for the purpose of this example.
self.messagesTableView?.dataSource = dataSource
self.messagesTableView?.delegate = dataSource
}
}
Check the frame of your table view. If the height or width is 0, tableView:cellForRowAtIndexPath: will not be called.
I have the same problem and I believe this to be a bug in UIKit.
I created a simple list data source and a small view Controller to test this and I can confirm that cellForRowAtIndexPath does not get called. 'numberOfRowsInSection' returns a value greater 0 and the tableView's frame is set correctly.
The same code works when put in the view controller. Maybe I'm missing something here, but I think this is a bug on Apple's side.
SimpleListDataSource.swift
import UIKit
class SimpleListDataSource : NSObject, UITableViewDataSource {
var items: [String]
var cellIdentifier: String
typealias CellConfiguration = (UITableViewCell, String) -> ()
var cellConfiguration: CellConfiguration
init(items: [String], cellIdentifier: String, cellConfiguration: CellConfiguration) {
self.items = items
self.cellIdentifier = cellIdentifier
self.cellConfiguration = cellConfiguration
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("Number of rows: \(self.items.count)")
return self.items.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
print(__FUNCTION__)
let cell = tableView.dequeueReusableCellWithIdentifier(self.cellIdentifier, forIndexPath: indexPath)
self.cellConfiguration(cell, self.items[indexPath.row])
return cell
}
}
ViewController.swift
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let data = ["a", "b", "c"]
let dataSource = SimpleListDataSource(items: data, cellIdentifier: "cell") { (cell, string) -> () in
cell.textLabel?.text = string
}
self.tableView.dataSource = dataSource
self.tableView.reloadData()
}
}

Dynamic datasource/delegates for UITableView in swift

I need to set up different objects based on certain conditions as the datasource & delegate for table view.
But I am not able to assign tableview's datasource/delegate as it throws some errors.
Cannot assign a value of type NSObject? to a value of type UITableViewDelegate?
I did check this Q&A but this did not work.
var dataSourceDelegate:NSObject?
class RootViewController: UIViewController {
...
override func viewDidLoad() {
dataSourceDelegate = TableDataSourceDelegate()
// Table View
tableView = UITableView()
tableView!.setTranslatesAutoresizingMaskIntoConstraints(false)
tableView!.dataSource = dataSourceDelegate
// Cannot assign a value of type NSObject? to a value of type UITableViewDataSource?
tableView!.delegate = dataSourceDelegate
// Cannot assign a value of type NSObject? to a value of type UITableViewDelegate?
view.addSubview(tableView!)
// Constraints
var views:[String:UIView] = ["table":tableView!]
var hTableConstraint = "H:|[table]|"
var vConstraint = "V:|[table]|"
view.addConstraintsToView([hTableConstraint, vConstraint], view: view, viewVariables: views)
}
...
}
This is the datasource/delegate class
class TableDataSourceDelegate:NSObject, UITableViewDataSource, UITableViewDelegate {
// MARK: Datasource
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return UITableViewCell()
}
// MARK: Delegates
}
NSObject? doesn't conforms to UITableViewDelegate, neither to UITableViewDataSource. You should create your protocol like
protocol GeneralDataSource: UITableViewDataSource, UITableViewDelegate {}
And then all data sources should conform that protocol.
class MyDataSource: NSObject, GeneralDataSource {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
return UITableViewCell()
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 2
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
}
Then you can use it like this
var myDataSource: GeneralDataSource?
override func viewDidLoad() {
super.viewDidLoad()
self.myDataSource = MyDataSource()
self.tableView.delegate = self.myDataSource
}
This is how your TableDataSourceDelegate should look like:
import UIKit
class TableDataSourceDelegate: NSObject {
}
extension TableDataSourceDelegate: UITableViewDataSource {
#objc func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 0
}
#objc func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "defaultCell")
cell.textLabel?.text = "test"
return cell
}
}
extension TableDataSourceDelegate: UITableViewDelegate {
// your delegate implementation here
}
And view controller implementation
import UIKit
// The typealias definition
typealias TVDataSourceDelegate = protocol<UITableViewDataSource, UITableViewDelegate>
class ViewController: UIViewController {
var dataSourceDelegate: TVDataSourceDelegate?
var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
dataSourceDelegate = TableDataSourceDelegate()
// Table View
tableView = UITableView()
tableView!.translatesAutoresizingMaskIntoConstraints = false
tableView!.dataSource = dataSourceDelegate
tableView!.delegate = dataSourceDelegate
view.addSubview(tableView!)
// other code ...
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
Although, I would recommend to separate dataSource and delegate objects (e.g. put the delegate protocol conforming code into your view controller's code.

UITableViewDataSource not being invoked in Swift, Xcode 7 beta 2

This is a bizarre one, & I'm quite happy to be told that I've missed something obvious, but I can't see it.
Firstly, the UITableViewDataSource methods are not even showing up in autocomplete, then they're not called at runtime. Weirdly, the UITableViewDelegate methods are working on both counts.
Here's the (cut-down) code -
class PopupTable : NSObject, UITableViewDataSource, UITableViewDelegate
{
private var tableView : UITableView
init(rect: CGRect)
{
tableView = UITableView(frame: rect, style: UITableViewStyle.Plain)
super.init()
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "PopupCell")
tableView.dataSource = self
tableView.delegate = self
}
func show()
{
if let mainWindow = UIApplication.sharedApplication().delegate?.window!
{
mainWindow.addSubview(tableView)
}
}
// MARK: TableView Delegate & DataSource
func numberOfSectionsInTableView(tableView: UITableView) -> Int
{
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
return myData.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let cell : UITableViewCell = tableView.dequeueReusableCellWithIdentifier("PopupCell")!
cell.detailTextLabel?.text = "some text"
return cell
}
}
I have no problems getting this to work in a UIViewController, but that's not what I want here.