Init custom UITableViewCell from nib without dequeueReusableCellWithIdentifier - ios

SWIFT
I need to make an array of cells. I have few custom cell classes (inheritated from UITableViewCell) with nib files.
How to init cell without registering nib in tableview and doing dequeueReusableCellWithIdentifier?
I did it like this, but don't think, that it will work:
var labelCell = CustomCellClass.initialize()

I'm inferring from the discussion in comments elsewhere that the reason you want to not allow cells to be dequeued and reused is that you're having trouble keeping track of user input captured in the cells.
The bottom line is that you really should allow the cells to be dequeued and reused and just handle that appropriately. If you're having problems with cells being reused, this can be resolved by separating the “model” (i.e. your data) from the “view” (i.e., the UIKit controls). This is the spirit of the model-view-controller pattern, but is true in any of those patterns that have separation of concerns (e.g., MVVP, MVP, etc.).
The key is that as values change in the cell, your cell should immediately tell the view controller so that the view controller can update the model immediately. Then, when the view controller needs to later do something with the value associated with a particular row, it doesn't retrieve it from the cell, but rather from its own model.
So, I might define a protocol for the cell to inform the table view that its text field changed:
protocol CustomCellDelegate: class {
func cell(_ cell: CustomCell, didUpdateTextField textField: UITextField)
}
And I'd then define a cell class that called that delegate:
class CustomCell: UITableViewCell {
weak var delegate: CustomCellDelegate?
#IBOutlet weak var customTextField: UITextField! // hook up outlet to this property in IB
#IBAction func didChangeValue(_ sender: UITextField) { // hook up "editing changed" action for the text field to this method in IB
delegate?.cell(self, didUpdateTextField: sender)
}
}
Now, the view controller will:
register the reuse identifier with the NIB in question;
in cellForRowAt, populate the text field and specify itself as the delegate for that cell; and
handle the didUpdateTextField method to update model if user changes anything.
Thus, something like:
class ViewController: UITableViewController {
var values = ["One", "Two", "Three"] // some initial values
private let cellIdentifier = "CustomCell"
override func viewDidLoad() {
super.viewDidLoad()
// if you’re using NIBs, you register them.
// obviously if using prototype cells in your storyboard, this isn’t necessary.
tableView.register(UINib(nibName: "CustomCell", bundle: nil), forCellReuseIdentifier: cellIdentifier) // or use cell prototype with storyboard identifer specified
}
}
// MARK: - UITableViewDataSource
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return values.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CustomCell
// populate cell and specify delegate
cell.delegate = self
cell.customTextField.text = values[indexPath.row]
return cell
}
}
// MARK: - CustomCellDelegate
extension ViewController: CustomCellDelegate {
func cell(_ cell: CustomCell, didUpdateTextField textField: UITextField) {
// when the cell tells us that its text field's value changed, update our own model
if let indexPath = tableView.indexPath(for: cell), let string = textField.text {
values[indexPath.row] = string
}
}
}
Many people might be inclined to simplify this further, by hooking the IBAction for the text field directly to a view controller method. That works, and eliminates the need for this protocol, but the problem is that you need to figure out with which row this particular UIKit control is associated. The common trick is to navigate up the view hierarchy to identify the appropriate cell (e.g. often the text field will be in a content view within the cell, so you grab textField.superview.superview as! UITableViewCell), but that feels a little fragile to me.
But regardless of this little detail, hopefully this illustrates the broader pattern. Rather than trying to have cells keep track of user input, you should have the cell (the “view”) update the controller of any data changes immediately, and the view controller then updates the model immediately, and you no longer need to worry about the cell reuse optimizations that iOS employs.
For Swift 2 renditions, see previous revision of this answer.

The very idea of a static table is that it is fully defined in IB. One solution is to copy-paste those cells from their isolated NIBs to the one containing the table.
A better solution is to make the table dynamic, and have the dynamic code return a static number of sections and rows. Write the rest of the logic as if it's dynamic, (e.g. register all of the nibs, and initialize an array of cell identifiers how you want them organized in the table, use that array to dequeue).

Another way to solve it:
1) UITextField Delegate in View Controller. Store values in array
var textFieldValues = ["", "", "", "", "", "", "", "", "", ""] // with count of text fields in table. Init in top of ViewController.
//MARK:TextField Delegate
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if string == " "
{
return false
}
let indexPathForCellWhereTextFieldIs = self.tableView.indexPathForCell(textField.superview?.superview as! UITableViewCell)
textFieldValues[(indexPathForCellWhereTextFieldIs?.section)!] = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString as String
return true
}
2) At indexPathForRow
cell.registrationTextField.text = textFieldValues[indexPath.section]
cell.registrationTextField.delegate = self
3) Get data from array
func tapOnRegisterButton(sender:UIButton)
{
debugPrint(textFieldValues)
}

Related

TableViewController / Custom cell - can't move to next text field

I have a table view controller with a custom cell which contains a text field - it's a form basically.
i want to automatically go to the next text field when users press "return" on their keyboard but for some reason my solution doesn't work.
In TableViewController, I do:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? CustomCell
cell?.box.tag = indexPath.row
In my custom table view cell, I have
override func awakeFromNib() {
super.awakeFromNib()
box.delegate = self
...
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if let nextField = textField.superview?.viewWithTag(textField.tag+1) as? UITextField {
nextField.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return true
}
The issue is that textField.superview?.viewWithTag(textField.tag+1) is always nil. I don't know why because I clearly set the tag and also mark it as a delegate. thank you.
Adding some clarity and more suggestions to the valid answer by #jawadAli, as I feel you are still new to iOS development.
You are trying to get the tableView from the textField. But you will not get it by referring to the superview of textField. Because the view hierarchy would be like this:
UITableView > UITableViewCell > contentView > Your text field.
There can also be some more views in the view hierarchy, so you need to keep traversing through the superview chain till you get the UITableView. And #jawadAli has posted the code on how to get it.
But overall that is an incorrect approach. You should use delegation. I.e. your cell should call a method when it has resigned as first responder. And your table view controller will receive that call.
Then your view controller has to get the next cell and make it the first responder.
And if this doesn't make any sense to you, then I would very strongly suggest that you learn about Delegation. It's ubiquitous in iOS' libraries.
EDIT:
Approach to use delegation.
Create a protocol, let's say CellDelegate that has a function like func didFinishDataCapture(forCell: UITableViewCell).
The cell will have a delegate property of type CellDelegate.
The controller will conform to CellDelegate and will set itself as the cell's delegate in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
Now in your cell, when you are done with the text field (which you would know as cell would be the text field's delegate), you call your own delegate's function i.e. delegate.didFinishDataCapture(forCell: self).
In your implementation of didFinishDataCapture in the controller, you will know which cell has finished with the data capture and can put the logic on what to do next.
It should be nil as textField.superview is your cell class ... and your cell class does not have the view with required Tag .. so it will return nil..
import UIKit
extension UIView {
func lookForSuperviewOfType<T: UIView>(type: T.Type) -> T? {
guard let view = self.superview as? T else {
return self.superview?.lookForSuperviewOfType(type: type)
}
return view
}
}
Get tableView through this extension like this
let tableView = self.view.lookForSuperviewOfType(type: UITableView.self)
your function will become
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
let tableView = self.view.lookForSuperviewOfType(type: UITableView.self)
if let cell = tableView?.cellForRow(at: IndexPath(row: textField.tag+1, section: 0)) as? CustomCell {
cell.box.becomeFirstResponder()
} else {
textField.resignFirstResponder()
}
return true
}

How to get section count and row count of textfield in tableview?

I have a text field in a tableView. I need to get the position of textfield but the problem is there are multiple section in it. I am able to get only one thing section or row using textfield.tag but I need both.
You can find the parent UIResponder of any class by walking up the UIResponder chain; both UITextField and UITableViewCell inherit from UIView, which inherits from UIResponder, so to get the parent tableViewCell of your textfield you can call this function on your textfield:
extension UIResponder {
func findParentTableViewCell () -> UITableViewCell? {
var parent: UIResponder = self
while let next = parent.next {
if let tableViewCell = parent as? UITableViewCell {
return tableViewCell
}
parent = next
}
return nil
}
}
Then once you have the tableViewCell, you just ask the tableView for its index path with tableView.indexPAth(for:)
You never need to use the tag field:
guard let cell = textField.findParentTableViewCell (),
let indexPath = tableView.indexPath(for: cell) else {
print("This textfield is not in the tableview!")
}
print("The indexPath is \(indexPath)")
You can use a variation of a previous answer that I wrote.
Use a delegate protocol between the cell and the tableview. This allows you to keep the text field delegate in the cell subclass, which enables you to assign the touch text field delegate to the prototype cell in Interface Builder, while still keeping the business logic in the view controller.
It also avoids the potentially fragile approach of navigating the view hierarchy or the use of the tag property, which has issues when cells indexes change (as a result of insertion, deletion or reordering), and which doesn't work where you need to know a section number as well as a row number, as is the case here.
CellSubclass.swift
protocol CellSubclassDelegate: class {
func textFieldUpdatedInCell(_ cell: CellSubclass)
}
class CellSubclass: UITableViewCell {
#IBOutlet var someTextField: UITextField!
var delegate: CellSubclassDelegate?
override func prepareForReuse() {
super.prepareForReuse()
self.delegate = nil
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool
self.delegate?.textFieldUpdatedInCell(self)
return yes
}
ViewController.swift
class MyViewController: UIViewController, CellSubclassDelegate {
#IBOutlet var tableview: UITableView!
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! CellSubclass
cell.delegate = self
// Other cell setup
}
// MARK: CellSubclassDelegate
func textFieldUpdatedInCell(_ cell: CellSubclass) {
guard let indexPath = self.tableView.indexPathForCell(cell) else {
// Note, this shouldn't happen - how did the user tap on a button that wasn't on screen?
return
}
// Do whatever you need to do with the indexPath
print("Text field updated on row \(indexPath.row) of section \(indexPath.section")
}
}
You can also see Jacob King's answer using a closure rather than a delegate pattern in the same question.

Swift: How to access a mutable array of strings from one UIViewController to a TableView cell file

I have one view controller named TableViewController and another customised cell called feed.swift
The cells are getting reused properly and I have put tags on various buttons as I wan't to know what button of what feed is pressed on.
In my cellForRowAtIndexPath I'm populating my username with json that I have parsed. It looks like this
cell.username.text = username[indexPath.row]
output-> ["andre gomes", "renato sanchez", "renato sanchez"]
Then I have tagged my username button like this
cell.usernamePress.tag = indexPath.row
This is going on in my TableViewController
In my feed.swift I'm checking if a button is pressed and printing out the tag assigned to that button
#IBAction func usernameBut(sender: AnyObject) {
print(usernamePress.tag)
}
output-> 2
Now I need to access the username array of TableViewController in feed.swift and do something like username[usernamePress.tag]
I tried making a global.swift file but I'm not able to configure it for an array of strings.
import Foundation
class Main {
var name:String
init(name:String) {
self.name = name
}
}
var mainInstance = Main(name: "hello")
Even after doing this I tried printing mainInstance.name and it returned hello even after changing it. I want a solution where the array of strings holds the values I set in TableViewController and I can be able to use them in feed.swift
Any suggestions would be welcome! I'm sorry if there are any similar question regarding this but I'm not able to figure out how to use it for a mutable array of strings
I suggest you don't use the array directly in your FeedCell but instead return the press-event back to your TableViewController where you handle the event. According to the MVC Scheme, which is the one Apple requests you to use (checkout Apples documentation), all your data-manipulation should happen in the Controller, which then prepares the Views using this data. It is not the View that is in charge to display the right values.
To solve your problem I would choose to pass back the press-event via the delegation-pattern, e.g. you create a FeedCellDelegate protocol that defines a function to be called when the button is pressed:
protocol FeedCellDelegate {
func feedCell(didPressButton button: UIButton, inCell cell: FeedCell)
}
Inside your FeedCell you then add a delegate property, which is informed about the event by the View:
class FeedCell {
var delegate: FeedCellDelegate?
...
#IBAction func pressedUsernameButton(sender: UIButton) {
delegate?.feedCell(didPressButton: sender, inCell: self)
}
}
If your TableViewController then conforms to the just defined protocol (implements the method defined in there) and you assign the ViewController as the View's delegate, you can handle the logic in the Controller:
class TableViewController: UITableViewController, FeedCellDelegate {
...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("FeedCell", forIndexPath: indexPath) as! FeedCell
cell.delegate = self
// Further setup
return cell
}
func feedCell(didPressButton button: UIButton, inCell cell: FeedCell) {
guard let indexPath = tableView.indexPathForCell(cell) else { return }
// Do your event-handling
switch (button.tag) {
case 2: print("Is username button")
default: print("Press not handled")
}
}
}
As you might recognize I changed your class name. A Feed sounds more like a Model-class whereas FeedCell implies its role to display data. It makes a programmer's life way easier if you choose self-explaining names for your classes and variables, so feel free to adapt that. :)
you should add a weak array property to the tableViewCell:
weak var userNameArray:[String]?
Then in your tableViewController pass the username array into the cell:
fun tableView(tableView:UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// create the cell, then...
if let array = self.username {
cell.userNameArray = array
}
}
Then you can use the array in the cell itself to populate its fields handle button taps, etc.

Notify UITableViewController when cell is programmatically selected

My table view allows multiple cell selection, where each cell sets itself as selected when a button inside the cell has been clicked (similar to what the gmail app does, see picture below). I am looking for a way to let the UITableViewController know that cells have been selected or deselected, in order to manually change the UINavigationItem. I was hoping there is a way to do this by using the delegate methods, but I cannot seem to find one. didSelectRowAtIndexPath is handling clicks on the cell itself, and should not affect the cell's selected state.
The most straight forward way to do this would be to create our own delegate protocol for your cell, that your UITableViewController would adopt. When you dequeue your cell, you would also set a delegate property on the cell to the UITableViewController instance. Then the cell can invoke the methods in your protocol to inform the UITableViewController of actions that are occurring and it can update other state as necessary. Here's some example code to give the idea (note that I did not run this by the compiler, so there may be typos):
protocol ArticleCellDelegate {
func articleCellDidBecomeSelected(articleCell: ArticleCell)
func articleCellDidBecomeUnselected(articleCell: ArticleCell)
}
class ArticleCell: UICollectionViewCell {
#IBAction private func select(sender: AnyObject) {
articleSelected = !articleSelected
// Other work
if articleSelected {
delegate?.articleCellDidBecomeSelected(self)
}
else {
delegate?.articleCellDidBecomeUnselected(self)
}
}
var articleSelected = false
weak var delegate: ArticleCellDelegate?
}
class ArticleTableViewController: UITableViewController, ArticleCellDelegate {
func articleCellDidBecomeSelected(articleCell: ArticleCell) {
// Update state as appropriate
}
func articleCellDidBecomeUnselected(articleCell: ArticleCell) {
// Update state as appropriate
}
// Other methods ...
override tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueCellWithIdentifier("ArticleCell", forIndexPath: indexPath) as! ArticleCell
cell.delegate = self
// Other configuration
return cell
}
}
I would have a function like 'cellButtomDidSelect' in the view controller and in 'cellForRowAtIndexPath', set target-action to the above mentioned function

how to determine which cell's textfield was just edited in a collection view?

What property can I access to find out?
I was learning some swift from this tutorial, and I decided to give myself some problems to do. One was that in the tutorial, to edit a name, there is a UIAlert. I want to get rid of that and instead, have UITextField in place of the UILabel, so the user could simply tap on a name, and edit it. I would use the textFieldDidEndEditing(textField: UITextField) function to update the model, which is a dictionary of names and picture filenames.
I set the view controller as the UITextFieldDelegate, I put in the function, but now I'm stuck, because although the text was updated just fine in one of the cells, I don't know how to tell which cell it happened in.
In this case, you probably can consider to subclass a UITextField to refer a Dictionary item.
class DictionaryTextField: UITextField {
var item: [String : AnyObject]?
}
Also create a subclass of UITableViewCell to hold above DictionaryTextField as an IBOutlet property.
class TextFieldTableViewCell: UITableViewCell {
#IBOutlet weak var textField: DictionaryTextField!
}
After finished above setting, an item Dictionary of datasource can be set in func tableView(:, cellForRowAtIndexPath: ).
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) as! TextFieldTableViewCell
cell.textField.delegate = self
let item = items[indexPath.row]
cell.textField.item = item
cell.textField.text = "TextField \(item["name"]!)"
return cell
}
Later, in the UITextFiedDelegate, cast the textField as DictionaryTextField. Then the item can be retrieved directly.
func textFieldDidBeginEditing(textField: UITextField) {
guard let textField = textField as? DictionaryTextField else {
return
}
print("Did begin editing: \(textField.item)")
}
The revised codes can be downloaded again with this link: https://db.tt/8j9ENf7b

Resources