Correct way to use UITableViewDataSource and Custom UITableViewCell Delegate - ios

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

Related

Prevent tableview from being reused (MVVM )

I know how to preserve the action we have done on UITableView, after scrolling back and forth.
Now Iam doing a simple UITableView on MVVM
which has a Follow button . like this.
Follow button changes to Unfollow after click and resets after scrolling.
Where and How to add the code to prevent this?
Here is the tableview Code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return Vm.personFollowingTableViewViewModel.count
}
var selectedIndexArray:[Int] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: FollowList_MVVM.PersonFollowingTableViewCell.identifier , for: indexPath) as? PersonFollowingTableViewCell else{
return UITableViewCell()
}
cell.configure(with: Vm.personFollowingTableViewViewModel[indexPath.row])
cell.delegate = self
return cell
}
and configure(with: ) function
#objc public func didTapButton(){
let defaultPerson = Person(name: "default", username: "default", currentFollowing: true, image: nil)
let currentFollowing = !(person?.currentFollowing ?? false)
person?.currentFollowing = currentFollowing
delegate?.PersonFollowingTableViewCell(self, didTapWith: person ?? defaultPerson )
configure(with: person ?? defaultPerson)
}
func configure(with person1 : Person){
self.person = person1
nameLabel.text = person1.name
usernameLabel.text = person1.username
userImageview.image = person1.image
if person1.currentFollowing{
//Code to change button UI
}
custom delegate of type Person is used
I guess your main issue is with Button title getting changed on scroll, so i am posting a solution for that.
Note-: Below code doesn’t follow MVVM.
Controller-:
import UIKit
class TestController: UIViewController {
#IBOutlet weak var testTableView: UITableView!
var model:[Model] = []
override func viewDidLoad() {
for i in 0..<70{
let modelObject = Model(name: "A\(i)", "Follow")
model.append(modelObject)
}
}
}
extension TestController:UITableViewDelegate,UITableViewDataSource{
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TestTableCell
cell.dataModel = model[indexPath.row]
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
}
extension TestController:Actions{
func followButton(cell: UITableViewCell) {
let indexPath = testTableView.indexPath(for: cell)
model[indexPath!.row].buttonTitle = "Unfollow"
testTableView.reloadRows(at: [indexPath!], with: .automatic)
}
}
class Model{
var name: String?
var buttonTitle: String
init(name: String?,_ buttonTitle:String) {
self.name = name
self.buttonTitle = buttonTitle
}
}
Cell-:
import UIKit
protocol Actions:AnyObject{
func followButton(cell:UITableViewCell)
}
class TestTableCell: UITableViewCell {
#IBOutlet weak var followButtonLabel: UIButton!
#IBOutlet weak var eventLabel: UILabel!
var dataModel:Model?{
didSet{
guard let model = dataModel else{
return
}
followButtonLabel.setTitle(model.buttonTitle, for: .normal)
eventLabel.text = model.name
}
}
weak var delegate:Actions?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func followAction(_ sender: Any) {
delegate?.followButton(cell:self)
}
}
To convert this into MVVM approach, there are few things you need to change and move out.
The loop I have in viewDidLoad shouldn’t be there. That will be some API call, and should be handled by viewModel, and viewModel can delegate that to other repository to handle or handle itself. Upon receiving response viewModel update its state and communicate with View (in our case tableView) to re-render itself.
Code in extension where I am updating model object shouldn’t be in controller (model[indexPath!.row].buttonTitle = "Unfollow"), that has to be done by viewModel, and once the viewModel state changes it should communicate with view to re-render.
The interaction responder (Button action) in Cell class, should delegate action to viewModel and not controller.
Model class should be in its own separate file.
In short viewModel handles the State of your View and it should be the one watching your model for updates, and upon change it should ask View to re-render.
There are more things you could do to follow strict MVVM approach and make your code more loosely coupled and testable. Above points might not be 100% correct I have just shared some basic ideas i have. You can check article online for further follow up.
The above answer works . But I have gone through what suggested by #Joakim Danielson to find what exactly happens when you are updating the View and Why it is not updating on ViewModel
So I made an update to delegate function
ViewController delegate function
func PersonFollowingTableViewCell1( _ cell: PersonFollowingTableViewCell, array : Person, tag : Int)
Here, I called the array in the Viewmodel and assigned the values of array in func argument to it.
like ViewModel().Vmarray[tag].currentFollow = array[tag].currentFollow

Adding another cell to tableview when done editing textview inside cell

I would just like to preface this with the fact that I don't use storyboards.
I have a tableview with multiple sections that are filled with tableViewcells I have created programmatically. These custom cells include a textfield with some placeholder text. What I want the user to be able to do is to tap on the textfield, type their entry, and then hit "Enter" to dismiss the keyboard and then create a new cell underneath the cell they just edited. This is very similar to the behaviour that happens in the reminders app.
I'm having a hard time trying to figure out how to access the tableview's data model (an array) and figuring out which section that cell is in, adding the new string to the array, and then adding another dumby cell that has the placeholder text.
First at all you have to create a way to communicate between your cell and view controller.
You can use delegate pattern or callbacks for this.
For example:
final class TextFieldCell: UITableViewCell {
// MARK: - IBOutlets
#IBOutlet weak var textField: UITextField!
// MARK: - Local variables
var callback: ((_ text: String) -> Void)?
// MARK: - Lyfecycle
override func awakeFromNib() {
super.awakeFromNib()
textField.delegate = self
}
}
Also don't forget to call our callback:
extension TextFieldCell: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
callback?(textField.text!)
return true
}
}
Great! Now we send our string from cell to controller!
Example of code for your view controller(simplified version):
class ViewController: UIViewController {
// MARK: - IBOutlets
#IBOutlet weak var tableView: UITableView!
// MARK: - Local variables
var titles = ["Hello", "world"]
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return titles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let textFieldCell = tableView.dequeueReusableCell(withIdentifier: "textFieldCell", for: indexPath) as! TextFieldCell
textFieldCell.textField.placeholder = titles[indexPath.row]
textFieldCell.callback = { [weak self] newTitle in // don't forget about memory leaks
guard let `self` = self else { return }
// calculating index path for new row
let newIndexPath = IndexPath(row: indexPath.row + 1, section: indexPath.section)
// appending a new row in table view
self.titles.append(newTitle)
self.tableView.insertRows(at: [newIndexPath], with: UITableView.RowAnimation.automatic)
}
return textFieldCell
}
}

How to use KVO to update tableViewCells based on underlying array element changes?

I have a table view representing an underlying array. The cells have a label and a slider which should show the value of the percentage property of the array.
I want to use key-value observing to update the label whenever the percentage property changes. (I know KVO is overkill in this example but eventually sliding one slider will affect the other cells including the position of the slider and the underlying array will be set from multiple places in the app and at any time so KVO is the way to go.)
I've had a bunch of help from this answer, but I can't get it to fire and update the label. I'm including all my code here. Not sure where I'm going wrong.
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, CustomCellDelegate {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
tableView.dataSource = self
tableView.delegate = self
for i in 0...4 {
items.append(Items(ID: i, percentage: 50))
}
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: myTableViewCell.ID) as? myTableViewCell {
cell.object = items[indexPath.row]
cell.mySlider.tag = indexPath.row
return cell
} else {
return UITableViewCell()
}
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
#IBAction func sliderValueChanged(_ sender: UISlider) {
items[sender.tag].percentage = Double(sender.value)
print("percentage at \(items[sender.tag].ID) is \(items[sender.tag].percentage)")
}
func didUpdateObject(for cell: UITableViewCell) {
if let indexPath = tableView.indexPath(for: cell) {
tableView.reloadRows(at: [indexPath], with: .automatic)
print("hello")
}
}
}
class myTableViewCell: UITableViewCell {
static let ID = "myCell"
weak var delegate: CustomCellDelegate?
private var token: NSKeyValueObservation?
var object: Items? {
willSet {
token?.invalidate()
}
didSet {
myLabel.text = "\(object?.percentage ?? 0)"
token = object?.observe(\.percentage) { [weak self] object, change in
if let cell = self {
cell.delegate?.didUpdateObject(for: cell)
}
}
}
}
override func awakeFromNib() {
super.awakeFromNib()
}
#IBOutlet weak var myLabel: UILabel!
#IBOutlet weak var mySlider: UISlider!
}
class Items: NSObject {
let ID: Int
#objc dynamic var percentage: Double
init(ID: Int, percentage: Double){
self.ID = ID
self.percentage = percentage
super.init()
}
}
var items: [Items] = []
protocol CustomCellDelegate: class {
func didUpdateObject(for cell: UITableViewCell)
}
To do the KVO in Swift 4, you have to declare the property as dynamic and call observe(_:options:changeHandler:) on that object, saving the resulting NSKeyValueObservation token. When that token falls out of scope (or replaced with another token), the original observer will automatically be removed.
In your case, you have your observer calling the delegate, which then reloads the cell. But you never appear to set that delegate property, so I suspect that method isn't getting called.
But this all seems a bit fragile. I'd be inclined to just update the label directly in the observer's changeHandler. I also think you can do a more direct updating of the cell (I'd put the "value changed" IBAction in the cell, not the table view), and eliminate that rather awkward use of the tag to identify which row in the model array had its slider updated (which can be problematic if you insert or delete rows).
So consider this object:
class CustomObject: NSObject {
let name: String
#objc dynamic var value: Float // this is the property that the custom cell will observe
init(name: String, value: Float) {
self.name = name
self.value = value
super.init()
}
}
You could then have a table view controller that populates an array of objects with instances of this model type. The details here are largely unrelated to the observation (which we'll cover below), but I include this just to provide a complete example:
class ViewController: UITableViewController {
var objects: [CustomObject]!
override func viewDidLoad() {
super.viewDidLoad()
// self sizing cells
tableView.estimatedRowHeight = 60
tableView.rowHeight = UITableViewAutomaticDimension
// populate model with random data
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
objects = (0 ..< 1000).map {
CustomObject(name: formatter.string(for: $0)!, value: 0.5)
}
}
}
// MARK: - UITableViewDataSource
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return objects?.count ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath) as! CustomCell
cell.object = objects[indexPath.row]
return cell
}
}
Having done that, you can now have the base class for your cell (a) update the model object if the slider changes; and (b) observe changes to that dynamic property, in this example updating the label when the value changes are observed in the model object:
class CustomCell: UITableViewCell {
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var valueLabel: UILabel!
#IBOutlet weak var valueSlider: UISlider!
static private let formatter: NumberFormatter = {
let _formatter = NumberFormatter()
_formatter.maximumFractionDigits = 2
_formatter.minimumFractionDigits = 2
_formatter.minimumIntegerDigits = 1
return _formatter
}()
private var token: NSKeyValueObservation?
weak var object: CustomObject? {
didSet {
let value = object?.value ?? 0
nameLabel.text = object?.name
valueLabel.text = CustomCell.formatter.string(for: value)
valueSlider.value = value
token = object?.observe(\.value) { [weak self] object, change in
self?.valueLabel.text = CustomCell.formatter.string(for: object.value)
}
}
}
#IBAction func didChangeSlider(_ slider: UISlider) {
object?.value = slider.value
}
}
That yields:
For more information, see the "Key-Value Observing" section of the Using Swift with Cocoa and Objective-C: Adopting Cocoa Patterns.
hi #sean problem is in UITableview cell class you have already make diSet Method , so you dont need to pass value for cell.lable and slider Just try below code
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: myTableViewCell.ID) as? myTableViewCell {
//pass the object to which you wanna add observer to cell
cell.object = items[indexPath.row]
return cell
} else {
return UITableViewCell()
}
}

Loop through a uitableviewcell on submit in swift 4

I am trying to access each value of a text field in a prototype cell within a UITableView on Submit. I know I should be doing this in a better way (model) but for now, I just need to access these fields and cannot find a way to do this in Swift 3/4. Would anyone be able to assist?
Code:
import UIKit
import Firebase
class FormTableViewController: UITableViewController {
var formLabels = [String]()
var formPlaceholders = [String]()
override func viewDidLoad() {
super.viewDidLoad()
FirebaseApp.configure()
formLabels = ["Name","Email","Password", "Phone"]
formPlaceholders = ["John Smith","example#email.com","Enter Password", "8585551234"]
tableView.estimatedRowHeight = 30
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return formLabels.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier:
"FormTableCell", for: indexPath)
as! FormTableViewCell
let row = indexPath.row
cell.formLabel.font =
UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
cell.formLabel.text = formLabels[row]
cell.formTextField.placeholder = formPlaceholders[row]
return cell
}
#IBAction func submitButtonPressed(_ sender: Any) {
// Need to do something with the Name, Email, Phone and Password fields here
}
}
You seem to acknowledge that updating the model directly probably makes sense. So why not do that? Just:
Have model collection for the responses;
Set up delegate for the text field in the cell;
Have cellForRowAt set that delegate; and
Make the table view controller conform to that class.
So, something quick and dirty, set up the cell to hook up editChanged event from the text field and set up protocol to inform the view controller:
protocol FormTableViewCellDelegate: class {
func fieldValueChanged(cell: UITableViewCell, textField: UITextField)
}
class FormTableViewCell: UITableViewCell {
weak var delegate: FormTableViewCellDelegate?
#IBOutlet weak var formLabel: UILabel!
#IBOutlet weak var formTextField: UITextField!
#IBAction func editingChanged(_ sender: UITextField) {
delegate?.fieldValueChanged(cell: self, textField: sender)
}
}
And then have the view controller set up model object and conform to your new protocol:
class FormTableViewController: UITableViewController {
var formLabels = [String]()
var formPlaceholders = [String]()
var values = [String?]()
override func viewDidLoad() {
super.viewDidLoad()
...
formLabels = ["Name","Email","Password", "Phone"]
formPlaceholders = ["John Smith","example#email.com","Enter Password", "8585551234"]
values = [nil, nil, nil, nil]
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "FormTableCell", for: indexPath) as! FormTableViewCell
let row = indexPath.row
cell.formLabel.font = .preferredFont(forTextStyle: .headline)
cell.formLabel.text = formLabels[row]
cell.formTextField.placeholder = formPlaceholders[row]
cell.formTextField.text = values[row]
cell.delegate = self // set the delegate, too
return cell
}
#IBAction func submitButtonPressed(_ sender: Any) {
print(#function, values)
}
}
// delegate protocol to update model as text fields change
extension FormTableViewController: FormTableViewCellDelegate {
func fieldValueChanged(cell: UITableViewCell, textField: UITextField) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
values[indexPath.row] = textField.text
}
}
Then that's it, your model is updated as the text fields are updated. Plus this has the advantage that it now supports cell reuse, conforms to MVC patterns, etc.
If you want to just loop through cells, you can create an array of ‘IndexPath’.
let array = (0..<formLabels.count).map { IndexPath(row: $0, section:0) }
After that you can loop over this array and access individual cell using tableview method:- tableView.cellForIndexPath
Hope this helps. (Not on my laptop, so didn’t test the syntax)

Setting a UITableView data source and delegate in separate file - swift

If I would like to have the same basic UITableView appearing in two different scenes, is it a good idea to use one datasource and delegate location for both tables?
I wanted to try this, but when I select the table view in IB and try to drag the line to a custom class of UITableView file, or even to another custom view controller, it will not connect. It only seems possible to make the current View Controller into the table's datasource and delegate(?).
I'm wondering if this is at least similar to this question, but even if it is, how is this done in swift (and perhaps there is a new way to do this).
Swift 4.1. You can create separate class and inherit it from UITableViewDataSource and UITableViewDelegate class. Here, I am implementing UITableViewDataSource() methods in DataSource class. You also need to confirm NSObject so that we don’t have to fiddle with the #objc and #class keywords because UITableViewDataSource is an Objective-C protocol.
import Foundation
import UIKit
class DataSource: NSObject, UITableViewDataSource {
var formData: [FormData]? = nil
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.formData?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let label = cell?.contentView.viewWithTag(100) as? UILabel
let type = self.formData![indexPath.row]
label?.text = type.placeHolder
return cell!
}
}
Now, We will set DataSource to UITableView. If we crate separate class then we have to pass data to DataSource class.
class ViewController: UIViewController {
#IBOutlet weak var tblView: UITableView!
var formData: [FormData]? = nil
var dataSource = DataSource()
override func viewDidLoad() {
super.viewDidLoad()
formData = FormData.array
dataSource.formData = formData // Pass data to DataSource class
tblView.dataSource = dataSource // Setting DataSource
}
}
In similar way you can implement UITableViewDelegate in separate class. The other way to separate DataSource and Delegate is by creating extension of your viewController. Even you can crate separate class where you can only define extensions for your view controller. In you define extension then you don't need to pass data.
class ViewController: UIViewController {
#IBOutlet weak var tblView: UITableView!
var formData: [FormData]? = nil
override func viewDidLoad() {
super.viewDidLoad()
formData = FormData.array
tblView.dataSource = self
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.formData?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
let label = cell?.contentView.viewWithTag(100) as? UILabel
let type = self.formData![indexPath.row]
label?.text = type.placeHolder
label?.backgroundColor = UIColor.gray
return cell!
}
}
Here is a code example showing different Datasource and delegates for UITableView.
Code in Swift
import UIKit
// MARK: Cell
class ItemCell: UITableViewCell{
var label: UILabel!
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
label = UILabel(frame: CGRect(x: 0, y: 0, width: 200, height: 20))
label.textColor = .black
label.backgroundColor = .yellow
contentView.addSubview(label)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: Main View Controller
class BlueViewController: UIViewController{
var tableView: UITableView!
var myDataSourse: MyTVDataSource!
var myDelegate: MyTVDelegate!
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .blue
tableView = UITableView()
myDataSourse = MyTVDataSource(tableView: tableView)
myDelegate = MyTVDelegate()
myDelegate.presentingController = self
tableView.dataSource = myDataSourse
tableView.delegate = myDelegate
tableView.register(ItemCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(tableView)
self.tableView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0),
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0),
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0)
])
}
}
extension BlueViewController: BluePresenting{
func currentSelected(_ indexPath: IndexPath) {
print(indexPath)
}
}
// MARK: TableViewDelegate
protocol BluePresenting: class {
func currentSelected(_ indexPath: IndexPath)
}
class MyTVDelegate: NSObject,UITableViewDelegate{
var presentingController: BluePresenting?
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
presentingController?.currentSelected(indexPath)
}
}
// MARK: TableView DataSource
class MyTVDataSource: NSObject, UITableViewDataSource{
private var tableView: UITableView
private var items = ["Item 1","item 2","item 3","Item 4"]
init(tableView: UITableView) {
self.tableView = tableView
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ItemCell(style: .default, reuseIdentifier: "Cell")
cell.label.text = items[indexPath.row]
return cell
}
}
You can implement a custom class object, and implement the UITableViewDataSource methods for this class.
#interface MyDataSource : NSObject <UITableViewDataSource>
//...
#end
And then, the UITableView has properties, delegate and dataSource.
Assign right objects to those properties.
MyDataSource ds = ... ///< Initialize the dataSource object.
self.tableView.dataSource = ds; ///< Let ds be the dataSource of `self.tableView`
self.tableView.delegate = .... ///< Assign the delegate, generally it is `self`.
Each Tableview should have its own Tableview controller. This is in accordance with the Model View Controller Design Pattern.
If the data in the the two tables are the same, you could have a common class serve as the dataSource.

Resources