I have two Buttons (like/dislike) that works like thumbs up and down voting system style. When I click on the button though, every cell gets updated. Is there a way to solve this so that only the one that gets clicked updated?
Also, How do I reload the cell so that I don't have to pull to refresh to update the value of the button.setTitle value?
Here is my code:
PFTableViewCell:
class UserFeedCell: PFTableViewCell {
#IBOutlet weak var likeButton: UIButton!
#IBOutlet weak var dislikeButton: UIButton!
var vote: Int = 0 // initialize to user's existing vote, retrieved from the server
var likeCount: Int = 0 // initialize to existing like count, retrieved from the server
var dislikeCount: Int = 0 // initialize to existing dislike count, retrieved from the server
#IBAction func dislikeButton(sender: UIButton) {
buttonWasClickedForVote(-1)
print(likeCount)
print(dislikeCount)
}
#IBAction func likeButton(sender: UIButton) {
buttonWasClickedForVote(1)
print(likeCount)
print(dislikeCount)
}
private func buttonWasClickedForVote(buttonVote: Int) {
if buttonVote == vote {
// User wants to undo her existing vote.
applyChange(-1, toCountForVote: vote)
vote = 0
}
else {
// User wants to force vote to toggledVote.
// Undo current vote, if any.
applyChange(-1, toCountForVote: vote)
// Apply new vote.
vote = buttonVote
applyChange(1, toCountForVote: vote)
}
}
private func applyChange(change: Int, toCountForVote vote: Int ) {
if vote == -1 { dislikeCount += change }
else if vote == 1 { likeCount += change }
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "TableViewCell" // your cell identifier name in storyboard
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! PFTableViewCell
cell.likeButton.selected = vote == 1
cell.dislikeButton.selected = vote == -1
cell.likeButton.titleLabel!.text = "\(likeCount)"
cell.dislikeButton.titleLabel!.text = "\(dislikeCount)"
return cell
}
You can do it with delegate mechanism
Create protocol for your action:
protocol LikeProtocol {
func likeOrDislikeActionAtRow(row: Int)
}
Make your TableViewController class confirm this protocol:
class YourTableViewController: UITableViewController, LikeProtocol {
...
func likeOrDislikeActionAtRow(row: Int) {
...
// Reload only you want cell
self.tableView.beginUpdates()
self.tableView.reloadRowsAtIndexPaths([NSIndexPath(forRow: row, inSection: 1)], withRowAnimation: UITableViewRowAnimation.Fade)
self.tableView.endUpdates()
...
}
...
}
Add to your PFTableViewCell delegate object with type on protocol and row variable:
var delegate: LikeProtocol?
var rowValue: Int?
Set delegate and row at your func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath):
cell.delegate = self
cell.row = indexPath.row
Call protocol method in your like and dislike actions:
#IBAction func likeButton(sender: UIButton) {
....
delegate!.likeOrDislikeActionAtRow(row!)
....
}
Related
I'm creating a quiz app with custom cells that include a label of questions and then an answer coming from a UISegmentedControl.
The values of the segmentedcontrols get changed when scrolling and this leads to an inaccurate score. I understand that this is due to UITableView reusing cells.
My tableview's datasource in my main vc is simply the labels for all my questions coming from a plist file.
The code for my custom tableviewcell class is
class QuestionsTableViewCell: UITableViewCell {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var selection: UISegmentedControl!
var question: String = "" {
didSet {
if (question != oldValue) {
questionLabel.text = question
}
}
}
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
}
//Just for testing
#IBAction func segmentChanged(_ sender: UISegmentedControl) {
print("value is ", sender.selectedSegmentIndex);
}
}
where the View is stored in an .XIB file.
And the code for my main vc is
class ViewController: UIViewController, UITableViewDataSource {
let questionsTableIdentifier = "QuestionsTableIdentifier"
#IBOutlet var tableView:UITableView!
var questionsArray = [String]();
var questionsCellArray = [QuestionsTableViewCell]();
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let path = Bundle.main.path(forResource:
"Questions", ofType: "plist")
questionsArray = NSArray(contentsOfFile: path!) as! [String]
tableView.register(QuestionsTableViewCell.self,
forCellReuseIdentifier: questionsTableIdentifier)
let xib = UINib(nibName: "QuestionsTableViewCell", bundle: nil)
tableView.register(xib,
forCellReuseIdentifier: questionsTableIdentifier)
tableView.rowHeight = 108;
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return questionsArray.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: questionsTableIdentifier, for: indexPath)
as! QuestionsTableViewCell
let rowData = questionsArray[indexPath.row]
cell.question = rowData
return cell
}
#IBAction func calculate(_ sender: UIButton) {
var score = 0
for cell in tableView.visibleCells as! [QuestionsTableViewCell] {
score += cell.selection.selectedSegmentIndex
}
let msg = "Score is, \(score)"
print(msg)
}
#IBAction func reset(_ sender: UIButton) {
for cell in tableView.visibleCells as! [QuestionsTableViewCell] {
cell.selection.selectedSegmentIndex = 0;
}
}
}
What I'd like to do is just keep track of all 'selection' changes of the Questions cells in an array, and then use that array in cellForRowAt. I'm just confused as to how i can dynamically keep track of changes from a view in another class. I'm new to Swift and would like to solve this is a proper MVC fashion. Thanks
Instead of a simple string array as data source create a class holding the text and the selected index
class Question {
let text : String
var answerIndex : Int
init(text : String, answerIndex : Int = 0) {
self.text = text
self.answerIndex = answerIndex
}
}
Declare questionArray as
var questions = [Question]()
Populate the array in viewDidLoad with
let url = Bundle.main.url(forResource: "Questions", withExtension: "plist")!
let data = try! Data(contentsOf: url)
let questionsArray = try! PropertyListSerialization.propertyList(from: data, format: nil) as! [String]
questions = questionsArray.map {Question(text: $0)}
In the custom cell add a callback and call it in the segmentChanged method passing the selected index, the property question is not needed, the label is updated in cellForRow of the controller
class QuestionsTableViewCell: UITableViewCell {
#IBOutlet weak var questionLabel: UILabel!
#IBOutlet weak var selection: UISegmentedControl!
var callback : ((Int) -> ())?
#IBAction func segmentChanged(_ sender: UISegmentedControl) {
print("value is ", sender.selectedSegmentIndex)
callback?(sender.selectedSegmentIndex)
}
}
In cellForRow add the callback and update the model in the closure
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: questionsTableIdentifier, for: indexPath) as! QuestionsTableViewCell
let question = questions[indexPath.row]
cell.questionLabel.text = question.text
cell.selection.selectedSegmentIndex = question.answerIndex
cell.callback = { index in
question.answerIndex = index
}
return cell
}
To reset the segmented controls in the cells set the property in the model to 0 and reload the table view
#IBAction func reset(_ sender: UIButton) {
questions.forEach { $0.answerIndex = 0 }
self.tableView.reloadData()
}
Now you could calculate the score directly from the model instead of the view.
Don't try to use cells to hold information. As the user scrolls through your table view, cells that scroll out of view will get recycled and their field settings will be lost. Also, newly dequeued cells will have the settings from the last time they were used.
You need to refactor your code to read/write information into a data model. Using an array of Structs as a data model is a reasonable way to go. (Or, as vadian suggests in his answer, and array of Class objects, so you get reference semantics.)
You have an IBAction segmentChanged() in your custom cell class. The next trick is to notify the view controller when the user changes the selection, and to update cells when you set them up in cellForRowAt.
I suggest defining a protocol QuestionsTableViewCellProtocol, and have the view controller conform to that protocol:
protocol QuestionsTableViewCellProtocol {
func userSelected(segmentIndex: Int, inCell cell: UITableViewCell)
}
}
Add a delegate property to your QuestionsTableViewCell class:
class QuestionsTableViewCell: UITableViewCell {
weak var delegate: QuestionsTableViewCellProtocol?
//The rest of your class goes here...
}
Update your cell's segmentChanged() method to invoke the delegate's userSelected(segmentIndex:inCell:) method.
In your view controller's cellForRowAt, set the cell's delegate to self.
func userSelected(segmentIndex: Int, inCellCell cell: UITableViewCell) {
let indexPath = tableView.indexPath(for: cell)
let row = indexPath.row
//The code below assumes that you have an array of structs, `dataModel`, that
//has a property selectedIndex that remembers which cell is selected.
//Adjust the code below to match your actual array that keeps track of your data.
dataModel[row].selectedIndex = segmentIndex
}
Then update cellforRowAt() to use the data model to set the segment index on the newly dequeued cell to the correct index.
Also update your calculate() function to look at the values in your dataModel to calculate the score, NOT the tableView.
That's a rough idea. I left some details out as "an exercise for the reader." See if you can figure out how to make that work.
Still very much a Swift noob, I have been looking around for a proper way/best practice to manage row deletions in my UITableView (which uses custom UserCells) based on tapping a UIButton inside the UserCell using delegation which seems to be the cleanest way to do it.
I followed this example: UITableViewCell Buttons with action
What I have
UserCell class
protocol UserCellDelegate {
func didPressButton(_ tag: Int)
}
class UserCell: UITableViewCell {
var delegate: UserCellDelegate?
let addButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Add +", for: .normal)
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
addSubview(addButton)
addButton.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -6).isActive = true
addButton.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
addButton.heightAnchor.constraint(equalToConstant: self.frame.height / 2).isActive = true
addButton.widthAnchor.constraint(equalToConstant: self.frame.width / 6).isActive = true
}
func buttonPressed(_ sender: UIButton) {
delegate?.didPressButton(sender.tag)
}
}
TableViewController class:
class AddFriendsScreenController: UITableViewController, UserCellDelegate {
let cellId = "cellId"
var users = [User]()
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return users.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! UserCell
cell.delegate = self
cell.tag = indexPath.row
return cell
}
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
where the Users in users are appended with a call to the database in the view controller.
My issues
The button in each row of the Table View is clickable but does not do anything
The button seems to be clickable only when doing a "long press", i.e. finger stays on it for a ~0.5s time
Will this method guarantee that the indexPath is updated and will not fall out of scope ? I.e. if a row is deleted at index 0, will deleting the "new" row at index 0 work correctly or will this delete the row at index 1 ?
What I want
Being able to click the button in each row of the table, which would remove it from the tableview.
I must be getting something rather basic wrong and would really appreciate if a Swift knight could enlighten me.
Many thanks in advance.
There are at least 3 issues in your code:
In UserCell you should call:
button.addTarget(self, action: #selector(buttonPressed), for: .touchUpInside)
once your cell has been instantiated (say, from your implementation of init(style:reuseIdentifier:)) so that self refers to an actual instance of UserCell.
In AddFriendsScreenController's tableView(_:cellForRowAt:) you are setting the tag of the cell itself (cell.tag = indexPath.row) but in your UserCell's buttonPressed(_:) you are using the tag of the button. You should modify that function to be:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
delegate?.didPressButton(self.tag)
}
As you guessed and as per Prema Janoti's answer you ought to reload you table view once you deleted a row as your cells' tags will be out of sync with their referring indexPaths. Ideally you should avoid relying on index paths to identify cells but that's another subject.
EDIT:
A simple solution to avoid tags being out of sync with index paths is to associate each cell with the User object they are supposed to represent:
First add a user property to your UserCell class:
class UserCell: UITableViewCell {
var user = User() // default with a dummy user
/* (...) */
}
Set this property to the correct User object from within tableView(_:cellForRowAt:):
//cell.tag = indexPath.row
cell.user = self.users[indexPath.row]
Modify the signature of your UserCellDelegate protocol method to pass the user property stored against the cell instead of its tag:
protocol UserCellDelegate {
//func didPressButton(_ tag: Int)
func didPressButtonFor(_ user: User)
}
Amend UserCell's buttonPressed(_:) action accordingly:
func buttonPressed(_ sender: UIButton) {
//delegate?.didPressButton(sender.tag)
//delegate?.didPressButton(self.tag)
delegate?.didPressButtonFor(self.user)
}
Finally, in your AddFriendsScreenController, identify the right row to delete based on the User position in the data source:
//func didPressButton(_ tag: Int) { /* (...) */ } // Scrap this.
func didPressButtonFor(_ user: User) {
if let index = users.index(where: { $0 === user }) {
let indexPath = IndexPath(row: index, section: 0)
users.remove(at: index)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
Note the if let index = ... construct (optional binding) and the triple === (identity operator).
This downside of this approach is that it will create tight coupling between your User and UserCell classes. Best practice would dictate using a more complex MVVM pattern for example, but that really is another subject...
There is a lot of bad/old code on the web, even on SO. What you posted has "bad practice" written all over it. So first a few pointers:
Avoid an UITableViewController at all cost. Have a normal view controller with a table view on it
Delegates should always be weak unless you are 100% sure what you are doing
Be more specific when naming protocols and protocol methods
Keep everything private if possible, if not then use fileprivate. Only use the rest if you are 100% sure it is a value you want to expose.
Avoid using tags at all cost
The following is an example of responsible table view with a single cell type which has a button that removes the current cell when pressed. The whole code can be pasted into your initial ViewController file when creating a new project. In storyboard a table view is added constraint left, right, top, bottom and an outlet to the view controller. Also a cell is added in the table view with a button in it that has an outlet to the cell MyTableViewCell and its identifier is set to "MyTableViewCell".
The rest should be explained in the comments.
class ViewController: UIViewController {
#IBOutlet private weak var tableView: UITableView? // By default use private and optional. Always. For all outlets. Only expose it if you really need it outside
fileprivate var myItems: [String]? // Use any objects you need.
override func viewDidLoad() {
super.viewDidLoad()
// Attach table viw to self
tableView?.delegate = self
tableView?.dataSource = self
// First refresh and reload the data
refreshFromData() // This is to ensure no defaults are visible in the beginning
reloadData()
}
private func reloadData() {
myItems = nil
// Simulate a data fetch
let queue = DispatchQueue(label: "test") // Just for the async example
queue.async {
let items: [String] = (1...100).flatMap { "Item: \($0)" } // Just generate some string
Thread.sleep(forTimeInterval: 3.0) // Wait 3 seconds
DispatchQueue.main.async { // Go back to main thread
self.myItems = items // Assign data source to self
self.refreshFromData() // Now refresh the table view
}
}
}
private func refreshFromData() {
tableView?.reloadData()
tableView?.isHidden = myItems == nil
// Add other stuff that need updating here if needed
}
/// Will remove an item from the data source and update the array
///
/// - Parameter item: The item to remove
fileprivate func removeItem(item: String) {
if let index = myItems?.index(of: item) { // Get the index of the object
tableView?.beginUpdates() // Begin updates so the table view saves the current state
myItems = myItems?.filter { $0 != item } // Update our data source first
tableView?.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade) // Do the table view cell modifications
tableView?.endUpdates() // Commit the modifications
}
}
}
// MARK: - UITableViewDelegate, UITableViewDataSource
extension ViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myItems?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as? MyTableViewCell {
cell.item = myItems?[indexPath.row]
cell.delegate = self
return cell
} else {
return UITableViewCell()
}
}
}
// MARK: - MyTableViewCellDelegate
extension ViewController: MyTableViewCellDelegate {
func myTableViewCell(pressedMainButton sender: MyTableViewCell) {
guard let item = sender.item else {
return
}
// Delete the item if main button is pressed
removeItem(item: item)
}
}
protocol MyTableViewCellDelegate: class { // We need ": class" so the delegate can be marked as weak
/// Called on main button pressed
///
/// - Parameter sender: The sender cell
func myTableViewCell(pressedMainButton sender: MyTableViewCell)
}
class MyTableViewCell: UITableViewCell {
#IBOutlet private weak var button: UIButton?
weak var delegate: MyTableViewCellDelegate? // Must be weak or we can have a retain cycle and create a memory leak
var item: String? {
didSet {
button?.setTitle(item, for: .normal)
}
}
#IBAction private func buttonPressed(_ sender: Any) {
delegate?.myTableViewCell(pressedMainButton: self)
}
}
In your case the String should be replaced by the User. Next to that you will have a few changes such as the didSet in the cell (button?.setTitle(item.name, for: .normal) for instance) and the filter method should use === or compare some id or something.
try this -
update didPressButton method like below -
func didPressButton(_ tag: Int) {
let indexPath = IndexPath(row: tag, section: 0)
users.remove(at: tag)
tableView.reloadData()
}
GOAL
To delete a row from the TableView called from customTableViewCell
I have a tableView in my CustomTableViewController which deque's a customTableViewCell.
In that customTableViewCell class i have a UIButton Action which needs to change its buttonTitle when pressed, accordingly and delete the row itself if required.For this i am passing the indexPath of that cell to a globalVariable in that customTableViewCell class, which i use to delete my row accessing through the instance of CustomTableViewController.
CODE
Code for retrieving the database from firebase and struct is unnecessary ,just included for reference.
CustomTableViewController which has embed tableView in it:-
class FriendsListController: UIViewController , UITableViewDelegate , UITableViewDataSource,addingFriendDelegate{
var profileFeed = [profileStruct]() //profileFeed is the an array of type struct, in which i am storing my retrieved database value's
var customCellHandler = FriendsTableViewCell()
override func viewDidLoad() {
super.viewDidLoad()
listTableView.delegate = self
listTableView.dataSource = self
customCellHandler.delegate = self
if friendListTable == true && profileFeed.isEmpty == true{
FIRControllerHandle.retrievingForTheFriendListTableView { (userName, lctn, ID) in
let temp = profileStruct.init(userName: userName, location: lctn, UID: ID)
self.profileFeed.insert(temp, atIndex: 0)
self.listTableView.reloadData()
self.presentViews()
}
}else if friendListTable == false && profileFeed.isEmpty == true{
print(profileFeed)
print(profileFeed.isEmpty)
FIRControllerHandle.retrievingForThePendingFriendRequests({ (userName, location, userId) in
if self.profileFeed.isEmpty == true{
let temp = profileStruct.init(userName: userName, location: location, UID: userId)
self.profileFeed.insert(temp, atIndex: 0)
self.listTableView.reloadData()
self.presentViews()
}
})
}
}
//Code for storing the data in a struct is just for reference , trivial w.r.t context of my question
}
//addindFriendDelegate methods
func deleteRowAtTheIndex(index: NSIndexPath){
listTableView.deleteRowsAtIndexPaths([index], withRowAnimation: .Fade)
}
//TableView delegate Functions.
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return profileFeed.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = listTableView.dequeueReusableCellWithIdentifier("friendsCell") as! FriendsTableViewCell
cell.userNameLabel.text = profileFeed[indexPath.row].profileName
cell.userUID = profileFeed[indexPath.row].profileUserId
cell.userName = profileFeed[indexPath.row].profileName
cell.userLocationLabel.text = profileFeed[indexPath.row].profileLocation
cell.currentUserName = currentuserName_initial
cell.friendsListBool = friendListTable
cell.cellIndexPath = indexPath //Sending the indexPath of that row
return cell
}
}
customTableViewCell class which responds to buttons action:-
protocol addingFriendDelegate {
func deleteRowAtTheIndex(index: NSIndexPath)
}
class FriendsTableViewCell: UITableViewCell {
var userName : String!
var userLocation : String!
var userUID : String!
var FIRControllerHandle : FIRController = FIRController() //I am using firebase as my backend and `FIRController` class handles all the firebase functions.
var delegate: addingFriendDelegate!
var friendsListBool : Bool = true // I am managing two tableViews datasource in one tableView , and `friendsListBool` tells me which node to retrieve from my database.
var currentUserName : String!
var cellIndexPath : NSIndexPath!
var listTableViewController : FriendsListController = FriendsListController() // Instance of viewController in which my tableView is embed
#IBAction func addFriendsBtnAction(sender: CustomButton) {
if friendsListBool{
FIRControllerHandle.sendFriendRequest(userUID, completionBlock: {
self.addFriendsBtn.setTitle("Sent", forState: .Normal) //Setting the title
print(self.cellIndexPath.row) //Printing Fine
self.listTableViewController.listTableView.deleteRowsAtIndexPaths([self.cellIndexPath], withRowAnimation: .Fade)
})
} else if !friendsListBool {
FIRControllerHandle.accepFriendRequest(currentUserName, friendID: userUID, friendName : userName ,completionBlock: {
self.addFriendsBtn.setTitle("Friends", forState: .Normal)
})
}
}
}
WHAT HAVE I TRIED
self.listTableViewController.listTableView.deleteRowsAtIndexPaths([self.cellIndexPath], withRowAnimation: .Fade)
called in #IBAction func addFriendsBtnAction(sender: CustomButton) where listTableViewController is Instance of viewController in which my tableView is embed, and cellIndexPath indexPath of the row i want to delete
LINE OF ERROR
self.listTableViewController.listTableView.deleteRowsAtIndexPaths([self.cellIndexPath], withRowAnimation: .Fade)
Error:- fatal error: unexpectedly found nil while unwrapping an Optional value
(lldb)
PS:- I am not particularly inclined towards protocol-delegate method for this.I am looking for an alternative.
Never initialize a view controller with the default initializer like FriendsListController().
It will create a brand new instance which is not the instance you expect in the view hierarchy.
An suitable alternative to protocol / delegate is a completion closure.
According to your latest code change the following:
Delete
var customCellHandler = FriendsTableViewCell()
var delegate: addingFriendDelegate!
Insert in cellForRowAtIndexPath
cell.delegate = self
Insert in deleteRowAtTheIndex
profileFeed.removeAtIndex(index.row)
I am using a variation of the technique mentioned in this post to add and remove table view cells dynamically.
Initially, the table view cells looks like this:
Then, I add a new cell to section 1. Section 1 is the section above the "RESULTS" section. So I expect the new cell to appear below the cell with the name "h". But no! It turns into this!
The new cell is added in section 2 (The "RESULTS" section) and is added below the cell with the name "b". What's even more surprising is that the second cell in section 2 has disappeared!
Here is how I add the cell:
I have an array of cells here:
var cells: [[UITableViewCell]] = [[], [], []]
each subarray in the array represents a section. In viewDidLoad, I added some cells to sections 0 to 2 by calling:
addCellToSection(1, cell: someCell)
addCellToSection is defined as
func addCellToSection(section: Int, cell: UITableViewCell) {
cells[section].append(cell)
tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: cells[section].endIndex - 1, inSection: section)], withRowAnimation: .Top)
}
And the table view data source methods are defined in the same way as the aforementioned post.
I tried to print the number of cells in each section when I add the cell:
print("no. of rows in section 1: \(self.tableView(tableView, numberOfRowsInSection: 1))")
print("no. of rows in section 2: \(self.tableView(tableView, numberOfRowsInSection: 2))")
And the printed values are consistent i.e. when I add a new cell, the no. of rows increase by 1. But the weird thing is that it keeps placing rows in the wrong position.
Extra info: how I create the cell:
I first dequeue the cells from the prototype cells. I then call viewWithTag to get the text fields that are in the cell and add them to a [(UITextField, UITextField)]. Don't know whether this matters.
Okay so first of all, you should never store UITableView cells in some custom collection. This is and should be done by iOS, not you.
The data you are using to populate the cells are stored in some model I presume?
Your tableView should register cells using either:
func registerClass(cellClass: AnyClass?, forCellReuseIdentifier identifier: String)
or
func registerNib(nib: UINib?, forCellReuseIdentifier identifier: String)
or using Prototype cells in the Xib/Storyboard.
I recommend this setup, or similar:
class MyModel {
/* holds data displayed in cell */
var name: String?
var formula: String?
init(name: String, formula: String) {
self.name = name
self.formula = formula
}
}
class MyCustomCell: UITableViewCell, UITextFieldDelegate {
static var nibName = "MyCustomCell"
#IBOutlet weak var nameTextField: UITextField!
#IBOutlet weak var formulaTextField: UITextField!
weak var model: MyModel?
override func awakeFromNib() {
super.awakeFromNib()
nameTextField.delegate = self
formulaTextField.delegate = self
}
func updateWithModel(model: MyModel) {
/* update labels, images etc in this cell with data from model */
nameTextField.text = model.name
formulaTextField.text = model.formula
self.model = model
}
/* This code only works if MyModel is a class, because classes uses reference type, and the value
of the name and formula properies are changed in the model stored in the dictionary */
func textFieldShouldEndEditing(textField: UITextField) -> Bool {
let newText = textField.text
switch textField {
case nameTextField:
model?.name = newText
case formulaTextField:
model?.formula = newText
default:
print("Needed by compiler..")
}
}
}
class MyController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var tableVieW: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
/* This is not needed if you are using prototype cells in the Xib or Storyboard.
Convenient to use nib name as cell identifier */
tableVieW.registerNib(UINib(nibName: MyCustomCell.nibName, bundle: nil), forCellReuseIdentifier: MyCustomCell.nibName)
tableVieW.delegate = self
tableVieW.dataSource = self
}
private var dictionaryWithModelsForSection: Dictionary<Int, [MyModel]>!
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
let sectionCount = dictionaryWithModelsForSection.keys.count
return sectionCount
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
guard let models: [MyModel] = modelsForSection(section) else {
return 0
}
let rowCount = models.count
return rowCount
}
private func modelsForSection(section: Int) -> [MyModel]? {
guard section < dictionaryWithModelsForSection.count else {
return nil
}
let models = dictionaryWithModelsForSection[section]
return models
}
private func modelAtIndexPath(indexPath: NSIndexPath) -> MyModel? {
guard let models = modelsForSection(indexPath.section) where models.count > indexPath.row else {
return nil
}
let model = models[indexPath.row]
return model
}
func addRowAtIndexPath(indexPath: NSIndexPath, withModel model: MyModel) {
add(model: model, atIndexPath: indexPath)
tableVieW.insertRowsAtIndexPaths([indexPath], withRowAnimation: .None)
}
private func add(model model: MyModel, atIndexPath indexPath: NSIndexPath) {
guard var models = modelsForSection(indexPath.section) where indexPath.row <= models.count else { return }
models.insert(model, atIndex: indexPath.row)
dictionaryWithModelsForSection[indexPath.section] = models
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(MyCustomCell.nibName, forIndexPath: indexPath)
return cell
}
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
guard let
cell = cell as? MyCustomCell,
model = modelAtIndexPath(indexPath) else { return }
cell.updateWithModel(model)
}
}
If you want to insert a cell you can use the method addRowAtIndexPath:withModel i wrote in MyController above, you need to call that from some function creating the corresponding model...
Setup (Swift 1.2 / iOS 8.4):
I have UITableView custom cell (identifier = Cell) inside UIViewController. Have two buttons (increment/decrement count) and a label (display count) inside the custom TableView cell.
Goal:
Update the label as we press the increase count or decrease count button.
At present I am able to get the button Tag and call a function outside of the CellForRowAtIndexPath. The button press increases and decreases the count. But I am not able to display the count update in the label.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:FoodTypeTableViewCell = self.tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! FoodTypeTableViewCell
cell.addBtn.tag = indexPath.row // Button 1
cell.addBtn.addTarget(self, action: "addBtn:", forControlEvents: .TouchUpInside)
cell.subBtn.tag = indexPath.row // Button 2
cell.subBtn.addTarget(self, action: "subBtn:", forControlEvents: .TouchUpInside)
cell.countLabel.text = // How can I update this label
return cell
}
func addBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
count = 1 + count
println(count)
return count
}
func subBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
if count == 0 {
println("Count zero")
} else {
count = count - 1
}
println(count)
return count
}
I have seen this question here and there but was not able to find a clear answer in Swift. I would really appreciate if you could help answer it clearly so that other people can not just copy, but clearly understand what is going on.
Thank you.
Here is a solution that doesn't require tags. I'm not going to recreate the cell exactly as you want, but this covers the part you are asking about.
Using Swift 2 as I don't have Xcode 6.x anymore.
Let's start with the UITableViewCell subclass. This is just a dumb container for a label that has two buttons on it. The cell doesn't actually perform any specific button actions, it just passes on the call to closures that are provided in the configuration method. This is part of MVC. The view doesn't interact with the model, just the controller. And the controller provides the closures.
import UIKit
typealias ButtonHandler = (Cell) -> Void
class Cell: UITableViewCell {
#IBOutlet private var label: UILabel!
#IBOutlet private var addButton: UIButton!
#IBOutlet private var subtractButton: UIButton!
var incrementHandler: ButtonHandler?
var decrementHandler: ButtonHandler?
func configureWithValue(value: UInt, incrementHandler: ButtonHandler?, decrementHandler: ButtonHandler?) {
label.text = String(value)
self.incrementHandler = incrementHandler
self.decrementHandler = decrementHandler
}
#IBAction func increment(sender: UIButton) {
incrementHandler?(self)
}
#IBAction func decrement(sender: UIButton) {
decrementHandler?(self)
}
}
Now the controller is just as simple
import UIKit
class ViewController: UITableViewController {
var data: [UInt] = Array(count: 20, repeatedValue: 0)
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! Cell
cell.configureWithValue(data[indexPath.row], incrementHandler: incrementHandler(), decrementHandler: decrementHandler())
return cell
}
private func incrementHandler() -> ButtonHandler {
return { [unowned self] cell in
guard let row = self.tableView.indexPathForCell(cell)?.row else { return }
self.data[row] = self.data[row] + UInt(1)
self.reloadCellAtRow(row)
}
}
private func decrementHandler() -> ButtonHandler {
return { [unowned self] cell in
guard
let row = self.tableView.indexPathForCell(cell)?.row
where self.data[row] > 0
else { return }
self.data[row] = self.data[row] - UInt(1)
self.reloadCellAtRow(row)
}
}
private func reloadCellAtRow(row: Int) {
let indexPath = NSIndexPath(forRow: row, inSection: 0)
tableView.beginUpdates()
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.endUpdates()
}
}
When the cell is dequeued, it configures the cell with the value to show in the label and provides the closures that handle the button actions. These controllers are what interact with the model to increment and decrement the values that are being displayed. After changing the model, it reloads the changed cell in the tableview.
The closure methods take a single parameter, a reference to the cell, and from this it can find the row of the cell. This is a lot more de-coupled than using tags, which are a very brittle solution to knowing the index of a cell in a tableview.
You can download a full working example (Requires Xcode7) from https://bitbucket.org/abizern/so-32931731/get/ce31699d92a5.zip
I have never seen anything like this before so I am not sure if this will be the correct way to do. But I got the intended functionality using the bellow code:
For people who find it difficult to understand:
The only problem we have in this is to refer to the TableView Cell. Once you figure out a way to refer the cell, you can interact with the cell components.
func addBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0) // This defines what indexPath is which is used later to define a cell
let cell = tableView.cellForRowAtIndexPath(indexPath) as! FoodTypeTableViewCell! // This is where the magic happens - reference to the cell
count = 1 + count
println(count)
cell.countLabel.text = "\(count)" // Once you have the reference to the cell, just use the traditional way of setting up the objects inside the cell.
return count
}
func subBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
let cell = tableView.cellForRowAtIndexPath(indexPath) as! FoodTypeTableViewCell!
if count == 0 {
println("Count zero")
} else {
count = count - 1
}
cell.countLabel.text = "\(count)"
println(count)
return count
}
I hope someone will benefit from this.
PLEASE CORRECT ME IF THERE IS SOME PROBLEM IN THIS SOLUTION OR THERE IS A BETTER/PROPER WAY TO DO THIS.
Use tableView.reloadData() to reload your tableView content each time you click a button.
let text = "something"
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell:FoodTypeTableViewCell = self.tableView!.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as! FoodTypeTableViewCell
cell.addBtn.tag = indexPath.row // Button 1
cell.addBtn.addTarget(self, action: "addBtn:", forControlEvents: .TouchUpInside)
cell.subBtn.tag = indexPath.row // Button 2
cell.subBtn.addTarget(self, action: "subBtn:", forControlEvents: .TouchUpInside)
cell.countLabel.text = something
return cell
}
func addBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
count = 1 + count
println(count)
something = "\(count)"
self.tableView.reloadData()
return count
}
func subBtn(sender: AnyObject) -> Int {
let button: UIButton = sender as! UIButton
if count == 0 {
println("Count zero")
} else {
count = count - 1
}
println(count)
something = "\(count)"
self.tableView.reloadData()
return count
}
Update1
After your comments ...
you have an array (one value for each food) like this, and whenever you click on a button, you take the index of the row the contains that button, then use that index to retrive the value of count from your array, then reload the table view content.