I'm working on moving the UITableViewDataSource outside of the UITableViewController. However I have some custom cells that have their own delegates, which then call on the tableView to reload.
I'm not sure what the correct way of handling this is. Here's what I have:
final class MyTableViewController: UITableViewController {
lazy var myTableViewDataSource: MyTableViewDataSource = { MyTableViewDataSource(myProperty: MyProperty) }()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = myTableViewDataSource
}
}
Cell
protocol MyTableViewCellDelegate: AnyObject {
func doSomething(_ cell: MyTableViewCellDelegate, indexPath: IndexPath, text: String)
}
final class MyTableViewCell: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var packageSizeTextField: UITextField!
weak var delegate: MyTableViewCellDelegate?
var indexPath = IndexPath()
override func awakeFromNib() {
super.awakeFromNib()
}
func configureCell() {
// configureCell...
}
func textFieldDidChangeSelection(_ textField: UITextField) {
print(#function)
delegate?.doSomething(self, indexPath: indexPath, text: textField.text ?? "")
}
}
DataSource
final class MyTableViewDataSource: NSObject, UITableViewDataSource {
var myProperty: MyProperty!
init(myProperty: MyProperty) {
self.myProperty = myProperty
}
// ...
func doSomething(_ cell: MyTableViewCell, indexPath: IndexPath, text: String) {
// ...
tableView.performBatchUpdates({
tableView.reloadRows(at: [indexPath], with: .automatic)
})
// ERROR - tableView doesn't exist
}
}
My question is, how do I gain access to the tableView that this class is providing the source for? Is it as simple as adding a reference to the tableView like this?
var tableView: UITableView
var myProperty: MyProperty!
init(myProperty: MyProperty, tableView: UITableView) {
self.myProperty = myProperty
self.tableView = tableView
}
One option is to have your MyTableViewController conform to your MyTableViewCellDelegate and then set the controller as the delegate in cellForRowAt in your dataSource class.
However, you may be much better off using a closure.
Get rid of your delegate and indexPath properties in your cell, and add a closure property:
final class MyTableViewCell: UITableViewCell, UITextFieldDelegate {
#IBOutlet weak var packageSizeTextField: UITextField!
override func awakeFromNib() {
super.awakeFromNib()
configureCell()
}
func configureCell() {
// configureCell...
packageSizeTextField.delegate = self
}
var changeClosure: ((String, UITableViewCell)->())?
func textFieldDidChangeSelection(_ textField: UITextField) {
print(#function)
changeClosure?(textField.text ?? "", self)
// delegate?.doSomething(self, indexPath: indexPath, text: textField.text ?? "")
}
}
Now, in your dataSource class, set the closure:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "mtvc", for: indexPath) as! MyTableViewCell
c.packageSizeTextField.text = myData[indexPath.row]
c.changeClosure = { [weak self, weak tableView] str, c in
guard let self = self,
let tableView = tableView,
let pth = tableView.indexPath(for: c)
else {
return
}
// update our data
self.myData[pth.row] = str
// do something with the tableView
//tableView.reloadData()
}
return c
}
Note that as you have your code written, the first tap in the textField will not appear to do anything, because textFieldDidChangeSelection will be called immediately.
Edit
Here's a complete example that can be run without any Storyboard connections.
The cell creates a label and a text field, arranging them in a vertical stack view.
Row Zero will have the text field hidden and its label text will be set to the concatenated strings from myData.
The rest of the rows will have the label hidden.
The closure will be called on .editingChanged (instead of textFieldDidChangeSelection) so it is not called when editing begins.
Also implements row deletion for demonstration purposes.
The first row will be reloaded when text is changed in any row's text field, and when a row is deleted.
Cell Class
final class MyTableViewCell: UITableViewCell, UITextFieldDelegate {
var testLabel = UILabel()
var packageSizeTextField = UITextField()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
configureCell()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configureCell()
}
func configureCell() {
// configureCell...
let stack = UIStackView()
stack.axis = .vertical
stack.translatesAutoresizingMaskIntoConstraints = false
testLabel.numberOfLines = 0
testLabel.backgroundColor = .yellow
packageSizeTextField.borderStyle = .roundedRect
stack.addArrangedSubview(testLabel)
stack.addArrangedSubview(packageSizeTextField)
contentView.addSubview(stack)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
stack.topAnchor.constraint(equalTo: g.topAnchor),
stack.leadingAnchor.constraint(equalTo: g.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: g.trailingAnchor),
stack.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
packageSizeTextField.addTarget(self, action: #selector(textChanged(_:)), for: .editingChanged)
}
var changeClosure: ((String, UITableViewCell)->())?
#objc func textChanged(_ v: UITextField) -> Void {
print(#function)
changeClosure?(v.text ?? "", self)
}
}
TableView Controller Class
class MyTableViewController: UITableViewController {
lazy var myTableViewDataSource: MyTableViewDataSource = {
MyTableViewDataSource()
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
tableView.register(MyTableViewCell.self, forCellReuseIdentifier: "mtvc")
tableView.dataSource = myTableViewDataSource
}
}
TableView DataSource Class
final class MyTableViewDataSource: NSObject, UITableViewDataSource {
var myData: [String] = [
" ",
"One",
"Two",
"Three",
"Four",
"Five",
"Six",
"Seven",
"Eight",
]
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
myData.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return indexPath.row != 0
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "mtvc", for: indexPath) as! MyTableViewCell
c.testLabel.isHidden = indexPath.row != 0
c.packageSizeTextField.isHidden = indexPath.row == 0
if indexPath.row == 0 {
myData[0] = myData.dropFirst().joined(separator: " : ")
c.testLabel.text = myData[indexPath.row]
} else {
c.packageSizeTextField.text = myData[indexPath.row]
}
c.changeClosure = { [weak self, weak tableView] str, c in
guard let self = self,
let tableView = tableView,
let pth = tableView.indexPath(for: c)
else {
return
}
// update our data
self.myData[pth.row] = str
// do something with the tableView
// such as reload the first row (row Zero)
tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
}
return c
}
}
Edit 2
There is a lot to discuss which goes beyond the scope of your question, but briefly...
First, as a general rule Classes should be as independent as possible.
your Cell should only handle its elements
your Data Source should only manage the data (and, of course, the necessary funds like returning cells, handling Edit commits, etc)
your TableViewController should, as might be expected, control the tableView
If you are only manipulating the data and wanting to reload specific rows, it's not that big of a deal for your DataSource class to get a reference to the tableView.
But, what if you need to do more than that? For example:
You don't want your Cell or DataSource class to act on the button tap and do something like pushing a new controller onto a nav stack.
To use the protocol / delegate pattern, you can "pass a delegate reference" through the classes.
Here's an example (with just minimum code)...
Two protocols - one for text change, one for button tap:
protocol MyTextChangeDelegate: AnyObject {
func cellTextChanged(_ cell: UITableViewCell)
}
protocol MyButtonTapDelegate: AnyObject {
func cellButtonTapped(_ cell: UITableViewCell)
}
The controller class, which conforms to MyButtonTapDelegate:
class TheTableViewController: UITableViewController, MyButtonTapDelegate {
lazy var myTableViewDataSource: TheTableViewDataSource = {
TheTableViewDataSource()
}()
override func viewDidLoad() {
super.viewDidLoad()
// assign custom delegate to dataSource instance
myTableViewDataSource.theButtonTapDelegate = self
tableView.dataSource = myTableViewDataSource
}
// delegate func
func cellButtonTapped(_ cell: UITableViewCell) {
// do something
}
}
The data source class, which conforms to MyTextChangeDelegate and has a reference to MyButtonTapDelegate to "pass to the cell":
final class TheTableViewDataSource: NSObject, UITableViewDataSource, MyTextChangeDelegate {
weak var theButtonTapDelegate: MyButtonTapDelegate?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let c = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! theCell
// assign custom delegate to cell instance
c.theTextChangeDelegate = self
c.theButtonTapDelegate = self.theButtonTapDelegate
return c
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func cellTextChanged(_ cell: UITableViewCell) {
// update the data
}
}
and the Cell class, which will call the MyTextChangeDelegate (the data source class) on text change, and the MyButtonTapDelegate (the controller class) when the button is tapped:
final class theCell: UITableViewCell, UITextFieldDelegate {
weak var theTextChangeDelegate: MyTextChangeDelegate?
weak var theButtonTapDelegate: MyButtonTapDelegate?
func textFieldDidChangeSelection(_ textField: UITextField) {
theTextChangeDelegate?.cellTextChanged(self)
}
func buttonTapped() {
theButtonTapDelegate?.cellButtonTapped(self)
}
}
So, having said all that...
Speaking in the abstract can be difficult. For your specific implementation, you may be digging yourself into a hole.
You mention "how to use a containerView / segmented control to switch between controllers" ... It might be better to create a "data manager" class, rather than a "Data Source" class.
Also, with a little searching for Swift Closure vs Delegate you can find a lot of discussion stating that Closures are the preferred approach these days.
I put a project up on GitHub showing the two methods. The functionality is identical --- one approach uses Closures and the other uses Protocol/Delegate pattern. You can take a look and dig through the code (tried to keep it straight-forward) to see which would work better for you.
https://github.com/DonMag/DelegatesAndClosures
I have a UITableView and I am trying to add some content (Whish) in my case. I have set up everything programmatically but somehow adding cells is not working for me.
This is my UITableView and custom Wish Class for my data:
import UIKit
class WhishlistTableViewController: UITableViewController {
public var wishList : [Wish]?
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(WhishCell.self, forCellReuseIdentifier: WhishCell.reuseID)
self.wishList?.append(Wish(withWishName: "Test"))
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return wishList?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: WhishCell.reuseID, for: indexPath)
let currentWish = self.wishList![indexPath.row]
cell.textLabel?.text = currentWish.wishName
return cell
}
}
class Wish: NSObject {
public var wishName : String?
init(withWishName name: String) {
super.init()
wishName = name
}
}
And this is how I am creating the WishlistTableView inside my other UIViewController:
let theTableView: WhishlistTableViewController = {
let v = WhishlistTableViewController()
v.view.layer.masksToBounds = true
v.view.layer.borderColor = UIColor.white.cgColor
v.view.layer.borderWidth = 2.0
v.view.translatesAutoresizingMaskIntoConstraints = false
return v
}()
and its constraints:
theTableView.view.topAnchor.constraint(equalTo: wishlistView.topAnchor, constant: 180.0),
theTableView.view.bottomAnchor.constraint(equalTo: wishlistView.bottomAnchor, constant: 0),
theTableView.view.leadingAnchor.constraint(equalTo: wishlistView.safeAreaLayoutGuide.leadingAnchor, constant: 30.0),
theTableView.view.trailingAnchor.constraint(equalTo: wishlistView.safeAreaLayoutGuide.trailingAnchor, constant: -30.0),
The TableView appears just fine with its constraints but adding cells is not working and I have no idea why.. Grateful for every help :)
Replace
public var wishList : [Wish]?
with
public var wishList = [Wish]()
As you never init wishList so adding item here wishList?.append won't do anything
self.wishList.append(Wish(withWishName: "Test"))
Also remove this as it's the default
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
i have one problem: my tableView doesn't show any data from realm database. But i guess the problem is in cell.
class ClientViewController: UITableViewController {
#IBOutlet var table: UITableView!
var clientsInfo: Results<Client> {
get {
return realm.objects(Client.self)
}
}
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(myCell.self, forCellReuseIdentifier: "cell")
table.delegate = self
table.dataSource = self
table.reloadData()
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: myCell.reuseIdentifier, for: indexPath) as! myCell
let info=clientsInfo[indexPath.row]
cell.todoLabel.text = info.firstName
return cell
}
}
class myCell: UITableViewCell {
#IBOutlet weak var label: UILabel!
static let reuseIdentifier = "cell"
#IBOutlet weak var todoLabel: UILabel!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
}
}
There is a few things wrong in your code,
First:
You're using a UITableViewController, by default this already has a tableView, so you don't need to declare a new tableView like you did
#IBOutlet var table: UITableView!
Second:
In you viewDidLoad you're registering the cell for the default tableView and using your declared table for dataSource and delegate, this won't work. I recommend you remove the table you create and use the default so your viewDidLoad would look like this:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(myCell.self, forCellReuseIdentifier: "cell")
self.tableView.delegate = self
self.tableView.dataSource = self
}
and Third:
you're missing the implementation of :
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
In this code you forgot to implement tableView(numberOfRowsInSection:) method, so it returns 0 by default and your tableview(cellForRowAt:) method never called.
AND
Maybe this is not about your question but;
You used UITableView variable(name is table) in UITableView. It's unneccessary, because UITableView already has a UITableView(name is tableView). And you'have registered your cell into tableView, but it seems you try to dequeue this cell from table.
The problem is that UITableView.dataSource works fine with extensions, but does not work with delegates.
I created new project and added just one UITableView to the storyboard.
Here is the code using extension:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
// tableView.dataSource = Delegate()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
extension ViewController: UITableViewDataSource {
//class Delegate: NSObject, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(1) // called several times
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print(2) // called
var cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "UITableViewCell")
}
return cell!
}
}
Using delegate:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
// tableView.dataSource = self
tableView.dataSource = Delegate()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
//extension ViewController: UITableViewDataSource {
class Delegate: NSObject, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(1) // called several times
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print(2) // doesn't called
var cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell")
if cell == nil {
cell = UITableViewCell(style: .default, reuseIdentifier: "UITableViewCell")
}
return cell!
}
}
Has anyone encountered such a problem?
You need to store your Delegate object in you view controller. The reason for this is that dataSource variable of UITableView is a weak variable (so as to prevent retain cycles) - it means that if it isn't stored somewhere in a strong variable, it will immediately get deallocated.
tableView.dataSource = Delegate()
Here you assign new instance of Delegate to a weak variable and nowhere else. Do something like this
class ViewController: UIViewController {
var delegate = Delegate()
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self.delegate
}
The dataSource and delegate are both weak. So your Delegate() object with no strong reference will be released after viewDidLoad().
If you want to use delegate this way, just var delegate: Delegate! in your ViewController. Then in the viewDidLoad() :
self.delegate = Delegate()
tableView.delegate = self.delegate
I can't figure out why my TableView is not showing. Its probably something stupid but all of my MenuItemStruct's are complete and not null and my table view seems set up correctly to me. There are no errors, but my cellForRowAt method is not getting called. Please help?
class MenuViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var menuNavigationDelegate: MenuNavigationDelegate?, menuManagerDelegate: MenuManagerDelegate?
var conferenceId = "conferenceId-1"
var isDataLoading = false
#IBOutlet
var menuTableView: UITableView?
var menuItems = Array<MenuItemStruct>()
#IBAction
func closeMenuTapped() {
self.menuManagerDelegate?.closeMenu()
}
override func viewDidLoad() {
self.menuTableView?.register(UINib(nibName: "MenuTableViewCell", bundle: nil), forCellReuseIdentifier: "menuCell")
self.isDataLoading = true
MenuDataManager.getMenuInformation(conferenceId: self.conferenceId) {
menuItems, response, error in
self.menuItems = menuItems!
self.isDataLoading = false
DispatchQueue.main.async {
self.menuTableView?.reloadData()
}
}
self.menuTableView?.reloadData()
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:MenuTableViewCell = menuTableView!.dequeueReusableCell(withIdentifier: "menuCell", for: indexPath) as! MenuTableViewCell
if (!self.isDataLoading) {
cell.setUpCellWithData(menuItem: menuItems[indexPath.row])
}
return cell
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if(!self.isDataLoading) {
return self.menuItems.count
}
return 0;
}
}
You need to set the delegate and datasource of your table view. You have included the UITableViewDataSource and UITableViewDelegate protocols, but have not assigned a delegate and datasource for your tableview in the code.
Try something like this in your viewDidLoad:
self.menuTableView.delegate = self
self.menuTableView.dataSource = self