Best way to communicate between UITableViewCell and UIViewController - uitableview

The code below works but there is probably a better way. My goal is to call a UIViewController function from the UITableCell when aborting from edit mode.
I am doing this by setting an instantiated UIViewController reference to every UITableViewCell and then calling a function, CancelDelete(), on the UITableViewCell state change.
The code seems inefficient since for every MyCell I first instantiate a placeholder MyViewContoller as the public variable and then replace it with reference to the UIViewController when the UITableView initializes.
Is there a better way to do this?
class MyCell : UITableViewCell
{
var previousState : UITableViewCellStateMask = UITableViewCellStateMask.allZeros
// This holds a reference to the parent view controller
// Seems wasteful to instantiate since it gets replaced
var controller:MyViewController = MyViewController()
// This is called when the user aborts edit mode
override func willTransitionToState(state: UITableViewCellStateMask) {
if state & UITableViewCellStateMask.ShowingEditControlMask != nil {
if previousState & UITableViewCellStateMask.ShowingDeleteConfirmationMask != nil {
// send notification to controller
controller.CancelDelete(self)
}
}
previousState = state
}
}
class MyViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// Give cell access to this controller
var cell:MyCell = tableView.dequeueReusableCellWithIdentifier("cell") as MyCell
cell.controller = self
cell.tag = indexPath.row
}
// This is called from the table cell
func CancelDelete(cell:MyCell) {
editButtons[cell.tag].hidden = false
}
}

Change the type of controller to MyViewController! instead of MyViewController. Also, set it to a default value of nil.
The declaration of controller should look like this:
var controller: MyViewController! = nil
If you have any questions about types that end with an exclamation mark (!), look at:
https://developer.apple.com/library/mac/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html
(in the section named Optionals).

Related

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.

Delegate Method to UItableViewCell Swift

I have a Social Network Feed in form UItableView which has a cell. Now each cell has an image that animates when an even is triggered. Now, This event is in form of a string, will be triggered at every cell. the options for the event are defined in another class(of type NSObject).
My issue:
I constructed a protocol delegate method in table view, which will be called whenever the event is triggered for each cell. Then, I define this function in UITableViewCell Class, since my the image will be animating on that.
All is working well but I am unable to figure out how to assign the delegate of TableView class to cell class. What I mean is, how can I use UITableView.delegate = self in cellView class. I have tried using a static variable, but it doesn't work.
I have been playing around the protocols for a while now but really unable to figure out a solution to this.
I hope I am clear. If not, I will provide with an example in the comments. I am sorry, This is a confidential project and I cant reveal all details.
If I understand you correctly, you are trying to make each of your cells conform to a protocol that belongs to their UITableView? If this is the case then this cannot be done. The Delegation design pattern is a one to one relationship, i.e only one of your UITableViewCells would be able to conform to the UITableView's delegate.
Delegation is a simple and powerful pattern in which one object in a program acts on behalf of, or in coordination with, another object. The delegating object keeps a reference to the other object—the delegate—and at the appropriate time sends a message to it. The message informs the delegate of an event that the delegating object is about to handle or has just handled. The delegate may respond to the message by updating the appearance or state of itself or other objects in the application, and in some cases it can return a value that affects how an impending event is handled. The main value of delegation is that it allows you to easily customize the behavior of several objects in one central object.
Quote from the Apple Docs
I would suggest that your UITableViewCell should call a block (Objective-C) or a closure (Swift) whenever your specified event is triggered to achieve what you are looking for. Set up this closure in your tableView:cellForRowAtIndexPath function.
EXAMPLE
TableViewController
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCellID", for: indexPath) as! MyTableViewCell
cell.eventClosure = {
//Do something once the event has been triggered.
}
return cell
}
TableViewCell
func eventTriggered()
{
//Call the closure now we have a triggered event.
eventClosure()
}
If I correctly understood your question, maybe this could help:
class ViewController: UIViewController, YourCustomTableDelegate {
#IBOutlet weak var tableView: YourCustomTableView!
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.customTableDelegate = self
}
// table delegate method
func shouldAnimateCell(at indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
cell.animate(...)
}
}
}
Try something like this:
Define your delegate protocol:
protocol CustomCellDelegate: class {
func animationStarted()
func animationFinished()
}
Define your CustomCell. Extremely important to define a weak delegate reference, so your classes won't retain each other.
class CustomCell: UITableViewCell {
// Don't unwrap in case the cell is enqueued!
weak var delegate: CustomCellDelegate?
/* Some initialization of the cell */
func performAnimation() {
delegate?.animationStarted()
UIView.animate(withDuration: 0.5, animations: {
/* Do some cool animation */
}) { finished in
self.delegate?.animationFinished()
}
}
}
Define your view controller. assign delegate inside tableView:cellForRowAt.
class ViewController: UITableViewDelegate, UITableViewDataSource {
/* Some view controller customization */
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: CustomCell.self)) as? CustomCell
cell.delegate = self
cell.performAnimation()
return cell
}
}

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

Can't unwrap optional. No UITableViewCell

I've a table view that has a custom table view cell in it. My problem is that when I try and assign a value to a variable in the custom UITableViewCell I get the stated error. Now, I think its because the said variable is not initialised, but it got me completely stumped.
This is the custom table cell:
import Foundation
import UIKit
class LocationGeographyTableViewCell: UITableViewCell
{
//#IBOutlet var Map : MKMapView;
#IBOutlet var AddressLine1 : UILabel;
#IBOutlet var AddressLine2 : UILabel;
#IBOutlet var County : UILabel;
#IBOutlet var Postcode : UILabel;
#IBOutlet var Telephone : UILabel;
var location = VisitedLocation();
func Build(location:VisitedLocation) -> Void
{
self.location = location;
AddressLine1.text = "test";
}
}
My cell for row at index path is:
override func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
{
var addressCell = tableView.dequeueReusableCellWithIdentifier("ContactDetail") as? LocationGeographyTableViewCell;
if !addressCell
{
addressCell = LocationGeographyTableViewCell(style: UITableViewCellStyle.Value1, reuseIdentifier: "ContactDetail");
}
addressCell!.Build(Location);
return addressCell;
}
As I say I'm completely baffled, the Build function calls the correct function in the UITableViewCell.
Any help will be gratefully appreciated.
Ta
I just made a simple project with your code, and I have a nil "AddressLine1" label, which causes the same error you have. I assume we have the same problem.
I solved it by adding the identifier "ContactDetail" to the prototype cell in my storyboard.
I also suggest that you change your code a little :
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
// This newer API ensures that you always get a cell, so no need for optionals.
// Also note the "let" that is preferred since you don't plan on changing addressCell
let addressCell = tableView.dequeueReusableCellWithIdentifier("ContactDetail", forIndexPath: indexPath) as LocationGeographyTableViewCell;
//addressCell.Build(Location); // the cell is a view, views should not know about model objects
addressCell.AddressLine1 = Location.addressLine1
// ... same for all labels, or do all that in a "configureCell" function
return addressCell;
}
I finally solved it. Instead of using the storyboard to define the UITableViewCell (as I'd done in the parent view controller) I created a Xib with the content and wired that up to the cell's class, referenced it in the TableViewController and allocated it in the cell created method.
viewDidLoad()
{
var nib = UINib(nibName: "LocationTableViewCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: "locationCell")
}
var cell:LocationGeographyTableViewCell = self.tableView.dequeueReusableCellWithIdentifier("locationCell") as LocationGeographyTableViewCell
cell.AddressLine1.text = "teetetette";
return cell;

Resources