2

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()
Nike Kov
  • 12,630
  • 8
  • 75
  • 122
  • Why "without registering nib" and why "without dequeueReusableCellWithIdentifier"? – danh Jul 08 '16 at 14:38
  • It doesn't matter in this question. But if u want to know just for interest - to make a static table. I need it to take values from textfields, segmented control and switchers which are placed in my cells. – Nike Kov Jul 08 '16 at 14:47
  • You would use the `loadNibNamed:owner:options:` method on `NSBundle` – dan Jul 08 '16 at 15:01
  • The tableview being static matters very much. – danh Jul 08 '16 at 15:02
  • Only dans answer this question?)) – Nike Kov Jul 08 '16 at 15:04
  • `let headerLabelCell : CellClassName = NSBundle.mainBundle().loadNibNamed("NIbName", owner: self, options: nil)` give an error `Cannot convert value of type '[AnyObject]!' to specified type 'CellClassName'` – Nike Kov Jul 08 '16 at 16:07

3 Answers3

5

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
0

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

danh
  • 62,181
  • 10
  • 95
  • 136
  • The problem is that when i want to take some values from cell, that are dequeued - they are nil. Your solution will work only when all cells would be displayed at the screen. – Nike Kov Jul 08 '16 at 15:12
  • 1
    Can you explain further? It's a common error to think of taking values *from* a cell. One only *gives* values *to* a cell. Those values are kept in a model (aka the datasource). – danh Jul 08 '16 at 15:15
  • It would be very good if i found how to take data from model (not from cell, becouse cell become nill when it go behind screen bounds). I did next way `let cell = self.tableView.cellForRowAtIndexPath(NSIndexPath.init(forRow: 0, inSection: NKVPhoneField)) as? NKVTextFieldTableVCell` and then getting value from cell `print(cell?.registrationTextField.text)` – Nike Kov Jul 08 '16 at 15:19
  • I see. If a cell contains a text field, you must make the view controller a delegate of that text field, and the delegate, as the text field is edited, must record the text in that field in your model. See the answer here (the "Alternatively" section in the answer) http://stackoverflow.com/a/27743391/294949 – danh Jul 08 '16 at 15:29
  • I know it. What should i do with segmented control? Do you know how to get information from cached cell's model? – Nike Kov Jul 08 '16 at 15:30
  • Exactly the same ( a little easier because it's just a bool value). Become the delegate, record changes in the model. What's really important to learn here is the idea of MVC in your app. The table view and its subviews are *views*. The flow is, user changes a cell's subview, delegate finds out about it, changes the model. Next time the cell is displayed, cellForRow... gets the (now updated) values from the model to set the cell's state. – danh Jul 08 '16 at 15:32
  • Ok, that can be the decision. But i want to find any another ways to solve it. It's interesting, that when i reuse cells, the values in textFields don't disappear. That means, that i can take them from cache or something like that? – Nike Kov Jul 08 '16 at 15:37
  • Yes, exactly. Cells are reused (deque "reusable"). These really are the only two ways to go about it. Either put your static definitions all in one NIB, or use a dynamic table (preferred in my opinion). – danh Jul 08 '16 at 15:41
  • I do like your second way now. But how to take values from cells? – Nike Kov Jul 08 '16 at 15:48
  • 1
    :-) you will only *give* values to cells. For a minute, forget about the table view. Make an array of objects that represents what the user will see. The controller's job is to give and take values from that array (and ONLY that array). It's other job is to reload the table view and configure the cells (when asked by cellForRowAtIndexPath) to match what's in that array. This is a very important idea in tableview programming, and in MVC in general. – danh Jul 08 '16 at 15:53
  • Thanks for your help. I understand what you are talking about) Thats why i ask previous question: When i type something in cell's textField, than reuse this cell, then cell appear again - the value saves. But i don't save it manually in my controller - it saves... somewhere. If i only can take value from this somewhere...) – Nike Kov Jul 08 '16 at 16:02
0

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)
}
Nike Kov
  • 12,630
  • 8
  • 75
  • 122