Table view cell checkmark changes position when scrolling - ios

I am new to Swift. I have created a simple list in a tableview. When the user long presses on a row, that row will get checked. It's working perfectly fine. But when I scroll down, check mark changes its position. I also tried to store position in NSMutableSet. But still it's not working. Maybe I am doing something wrong.
This is my code:
This method gets called on long press.
func longpress(gestureRecognizer: UIGestureRecognizer)
{
let longpress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longpress.state
let locationInview = longpress.location(in: tableview1)
var indexpath=tableview1.indexPathForRow(at: locationInview)
if(gestureRecognizer.state == UIGestureRecognizerState.began)
{
if(tableview1.cellForRow(at: indexpath!)?.accessoryType ==
UITableViewCellAccessoryType.checkmark)
{
tableview1.cellForRow(at: indexpath!)?.accessoryType =
UITableViewCellAccessoryType.none
}
else{
tableview1.cellForRow(at: indexpath!)?.accessoryType =
UITableViewCellAccessoryType.checkmark
}
}
}

The problem is that cells are reused and when you update a checkmark, you're updating a cell, but not updating your model. So when a cell scrolls out of view and the cell is reused, your cellForRowAt is obviously not resetting the checkmark for the new row of the table.
Likewise, if you scroll the cell back into view, cellForRowAt has no way of knowing whether the cell should be checked or not. You have to
when you detect your gesture on the cell, you have to update your model to know that this row's cell should have a check; and
your cellForRowAt has to look at this property when configuring the cell.
So, first make sure your model has some value to indicate whether it is checked/selected or not. In this example, I'll use "Item", but you'd use a more meaningful type name:
struct Item {
let name: String
var checked: Bool
}
Then your view controller can populate cells appropriately in cellForRowAt:
class ViewController: UITableViewController {
var items: [Item]!
override func viewDidLoad() {
super.viewDidLoad()
addItems()
}
/// Create a lot of sample data so I have enough for a scrolling view
private func addItems() {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
items = (0 ..< 1000).map { Item(name: formatter.string(from: NSNumber(value: $0))!, checked: false) }
}
}
extension ViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemCell", for: indexPath) as! ItemCell
cell.delegate = self
cell.textLabel?.text = items[indexPath.row].name
cell.accessoryType = items[indexPath.row].checked ? .checkmark : .none
return cell
}
}
Now, I generally let the cell handle stuff like recognizing gestures and inform the view controller accordingly. So create a UITableViewCell subclass, and specify this as the base class in the cell prototype on the storyboard. But the cell needs some protocol to inform the view controller that a long press took place:
protocol ItemCellDelegate: class {
func didLongPressCell(_ cell: UITableViewCell)
}
And the table view controller can handle this delegate method, toggling its model and reloading the cell accordingly:
extension ViewController: ItemCellDelegate {
func didLongPressCell(_ cell: UITableViewCell) {
guard let indexPath = tableView.indexPath(for: cell) else { return }
items[indexPath.row].checked = !items[indexPath.row].checked
tableView.reloadRows(at: [indexPath], with: .fade)
}
}
Then, the UITableViewCell subclass just needs a long press gesture recognizer and, upon the gesture being recognized, inform the view controller:
class ItemCell: UITableViewCell {
weak var delegate: CellDelegate?
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
addGestureRecognizer(longPress)
}
#IBAction func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
if gesture.state == .began {
delegate?.didLongPressCell(self)
}
}
}
By the way, by having the gesture on the cell, it avoids confusion resulting from "what if I long press on something that isn't a cell". The cell is the right place for the gesture recognizer.

You are not storing the change anywhere.
To avoid using too much memory, the phone reuses cells and asks you to configure them in the TableView's dataSource.
Let's say you have an array called data that has some structs that store what you want to show as cells. You would need to update this array and tell the tableView to reload your cell.
func userDidLongPress(gestureRecognizer: UILongPressGestureRecognizer) {
// We only care if the user began the longPress
guard gestureRecognizer.state == UIGestureRecognizerState.began else {
return
}
let locationInView = gestureRecognizer.location(in: tableView)
// Nothing to do here if user didn't longPress on a cell
guard let indexPath = tableView.indexPathForRow(at: locationInView) else {
return
}
// Flip the associated value and reload the row
data[indexPath.row].isChecked = !data[indexPath.row].isChecked
tableView.reloadRows(at: [indexPath], with: .automatic)
}
And always set the accessory when you configure a cell:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "someIdentifier",
for: indexPath
)
cell.accessoryType = data[indexPath.row].isChecked ? .checkmark : .none
}

Related

UITableViewCell delegate indexPath

Good time of day, I'm new in iOS development. I'm developing project where i have tableViewCell and button,progressBar on it. When i tap button indexPath of this cell is passed through delegate to viewController and then with another method i'm
downloading some data and show it's progress in progressBar. So when i tap to one cell and then to another, progress in first cell stops and continues in second, could anyone help? Thanks in advance )
Here is delegate methods in viewController:
func didTouchButtonAt(_ indexPath: IndexPath) {
songs[indexPath.row].isTapped = true
let selectedSong = self.songs[indexPath.row] as Song
DownloadManager.shared.delegate = self
self.indexQueue.append(indexPath)
self.selectedIndexPath = indexPath
DownloadManager.shared.download(url: selectedSong.url , title: selectedSong.title)
}
func downloadProgress(_ progress: Progress) {
if (progress.completedUnitCount) < progress.totalUnitCount {
selectedIndexPath = indexQueue.first
}
else if(!indexQueue.isEmpty){
indexQueue.removeFirst()
}
print(progress.fractionCompleted)
print(progress.completedUnitCount, progress.totalUnitCount)
print(indexQueue.count)
var cell: ViewControllerTableViewCell?
cell = self.tableView.cellForRow(at: self.selectedIndexPath!) as? ViewControllerTableViewCell
if cell != nil {
cell?.progress = Float(progress.fractionCompleted)
}
}
this is cell:
#IBAction func downloadButtonTouched(sender: Any){
self.delegate?.didTouchButtonAt(self.indexPath!)
self.progressBar.isHidden = false
}
As #RakshithNandish mentioned, I'v used list of indexPathes and when i tap to button, list adds indexPath. So, before passing progress to cell i check if progress is completed: if not, pass progress to first element of queue, otherwise just delete first element from queue, works fine.
You can create a model that may be an array which will hold the indexpath of tapped button's cell i.e. append indexpath to the array whenever button is tapped and remove it whenever you want. Later on while returning cells in cellForRowAtIndexPath you check if the array contains indexPath for which you are returning cell.
class DemoCell: UITableViewCell {
#IBOutlet button: UIButton!
}
class DemoTableViewController: UITableViewController {
var buttonTappedIndexPaths: [IndexPath] = []
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: DemoCell.className, for: indexPath) as! DemoCell
if buttonTappedIndexPaths.contains(indexPath) {
//show progress view spinning or whatever you want
} else {
//don't show progress view
}
}
}

Detecting double and single tap on UITableViewCell to perform two action

I have a PFQueryTableViewController in which I am trying to learn some tableView interactions.
What I currently have: The tableView cell increases its height on a tap didSelectRowAtIndexPath or I can basically segue cell related data to next viewController using performSegueWithIdentifier together with prepareForSegue using a single tap.
In the below code, if I comment the "Tap code to increases height of tableView cell" code, I can detect double tap. Other wise the single tap increases the height as the code is inside didSelect block.
What I need:
On Single tap, cell height increases or decrease. While the cell height is increased, if I double tap, it should segue cell data to next UIViewController.
If not, the single tap should increase or decrease the height. And double tap should segue cell data to next UIViewController
Code is as below:
var selectedCellIndexPath: NSIndexPath?
var SelectedCellHeight = CGFloat() // currently set to 480.0 tableView height
var UnselectedCellHeight = CGFloat() // currently set to 300.0 tableView unselected hight
....
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == indexPath {
return SelectedCellHeight
}
}
return UnselectedCellHeight
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! DataViewTableViewCell!
if cell == nil {
cell = DataViewTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
}
// Below is the double Tap gesture recognizer code in which
// The single tap is working, double tap is quite difficult to get
// as the cell height increases on single tap of the cell
let doubleTap = UITapGestureRecognizer()
doubleTap.numberOfTapsRequired = 2
doubleTap.numberOfTouchesRequired = 1
tableView.addGestureRecognizer(doubleTap)
let singleTap = UITapGestureRecognizer()
singleTap.numberOfTapsRequired = 1
singleTap.numberOfTouchesRequired = 1
singleTap.requireGestureRecognizerToFail(doubleTap)
tableView.addGestureRecognizer(singleTap)
// Tap code to increases height of tableView cell
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == indexPath {
self.selectedCellIndexPath = nil
cell.postComment.fadeIn()
cell.postCreatorPicture.alpha = 1
cell.postDate.fadeIn()
// cell.postTime.fadeIn()
cell.postCreator.fadeIn()
} else {
self.selectedCellIndexPath = indexPath
}
} else {
selectedCellIndexPath = indexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
func doubleTap() {
print("Double Tap")
}
func singleTap() {
print("Single Tap")
}
The other way I can make it work is, if I can get NSIndexPath of selected cell outside of didSelectRowAtIndexPath code, I can basically use the double tap to perform if else to get the required action. Can you get NSIndexPath outside the tableView default blocks?
Got it working!
First: Define var customNSIndexPath = NSIndexPath() and add the below code inside the didSelectRowAtIndexPath code block.
customNSIndexPath = indexPath
print(customNSIndexPath)
Second: Remove "Tap code to increases height of tableView cell" code from didSelectRowAtIndexPath and add to the singleTap() function. And perform segue using the doubleTap().
func doubleTap() {
print("Double Tap")
performSegueWithIdentifier("MainToPost", sender: self)
}
func singleTap() {
print("Single Tap")
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! DataViewTableViewCell!
if cell == nil {
cell = DataViewTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
}
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == customNSIndexPath {
self.selectedCellIndexPath = nil
} else {
self.selectedCellIndexPath = customNSIndexPath
}
} else {
selectedCellIndexPath = customNSIndexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
This is how you screw up working late without sleeping. It was the easiest thing to do. Thanks
Note: If someone has better solution or this one is wrong in some xyz case, please do comment. If yours is better it would really help others.
var lastClick: TimeInterval = 0.0
var lastIndexPath: IndexPath? = nil
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let now: TimeInterval = Date().timeIntervalSince1970
if (now - lastClick < 0.3) && (lastIndexPath?.row == indexPath.row ) {
print("Double Tap!")
} else {
print("Single Tap!")
}
lastClick = now
lastIndexPath = indexPath
}

tableView.cellForRowAtIndexPath returns nil with too many cells (swift)

So I have the weirdest thing;
I am looping a tableView in order to iterate over all cells. It works fine with less than 5 cells, but crashes with "unexpectedly found nil" for more cells. Here's the code:
for section in 0..<tableView.numberOfSections {
for row in 0..<tableView.numberofRowsInSection(section) {
let indexPath = NSIndexPath(forRow: row, inSection: section)
let cell = tableView?.cellForRowAtIndexPath(indexPath) as? MenuItemTableViewCell
// extract cell properties
The last line is the one that gives the error.
Any thoughts?
Because cells are reused, cellForRowAtIndexPath will give you cell only if cell for given indexPath is currently visible. It is indicated by the optional value. If you want to prevent from crash, you should use if let
if let cell = tableView?.cellForRowAtIndexPath(indexPath) as? MenuItemTableViewCell {
// Do something with cell
}
If you want to update values from cell, your cells should update the dataSource items. For example you can create delegate for that
protocol UITableViewCellUpdateDelegate {
func cellDidChangeValue(cell: UITableViewCell)
}
Add delegate to your cell and suppose we have a textField in this cell. We add target for the didCHangeTextFieldValue: for EditingDidChange event so it is called every time the user types somethink in it. And when he do, we call the delegate function.
class MyCell: UITableViewCell {
#IBOutlet var textField: UITextField!
var delegate: UITableViewCellUpdateDelegate?
override func awakeFromNib() {
textField.addTarget(self, action: Selector("didCHangeTextFieldValue:"), forControlEvents: UIControlEvents.EditingChanged)
}
#IBAction func didCHangeTextFieldValue(sender: AnyObject?) {
self.delegate?.cellDidChangeValue(cell)
}
}
Then in cellForRowAtIndexPath you add the delegate
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MyCellIdentifier", forIndexPath: indexPath)
cell.delegate = self
return cell
}
And finally we implement the delegate method:
func cellDidChangeValue(cell: UITableViewCell) {
guard let indexPath = self.tableView.indexPathForCell(cell) else {
return
}
/// Update data source - we have cell and its indexPath
}
Hope it helps

How to send cell number to the function in TableViewCell

I'd like to know how to get cell number(indexPath.row) in the following tapPickView function. topView is on the UITableViewCell, and pickView is on the topView. If pickView is tapped, tapPickView is activated.
override func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier(
"QuestionAndAnswerReuseIdentifier",
forIndexPath: indexPath) as! QuestionAndAnswerTableViewCell
cell.topView.pickView.userInteractionEnabled = true
var tap = UITapGestureRecognizer(target: self, action: "tapPickView")
cell.topView.pickView.addGestureRecognizer(tap)
return cell
}
func tapPickView() {
answerQuestionView = AnswerQuestionView()
answerQuestionView.questionID = Array[/*I wanna put cell number here*/]
self.view.addSubview(answerQuestionView)
}
First of all, you need to append : sign to your selector upon adding gesture recognizer in order for it to get the pickView as its parameter.
var tap = UITapGestureRecognizer(target: self, action: "tapPickView:")
Besides that, cells are reusable objects, so you should prevent adding same gesture again and again to the same view instance by removing previously added ones.
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("QuestionAndAnswerReuseIdentifier", forIndexPath: indexPath) as! QuestionAndAnswerTableViewCell
cell.topView.pickView.userInteractionEnabled = true
cell.topView.pickView.tag = indexPath.row
for recognizer in cell.topView.pickView.gestureRecognizers ?? [] {
cell.topView.pickView.removeGestureRecognizer(recognizer)
}
cell.topView.pickView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "tapPickView:"))
return cell
}
While populating the cell, you can set tag value of the pickView as indexPath.row so you can easily query that by cellForRowAtIndexPath(_:).
cell.topView.pickView.tag = indexPath.row
Assuming you already know the section of the cell you tap on. Let's say it is 0.
func tapPickView(recognizer: UIGestureRecognizer) {
let indexPath = NSIndexPath(forRow: recognizer.view.tag, inSection: 0)
if let cell = self.tableView.cellForRowAtIndexPath(indexPath) {
print("You tapped on \(cell)")
}
}
Hope this helps.
Assuming that this was not as simple as didSelectRowAtIndexPath, which I strongly recommend to first look into, passing the information to your method could look like this:
#IBAction func tapPickView:(sender: Anyobject) {
if let cell = sender as? UITableViewCell {
let indexPath = self.tableView.indexPathForCell(cell: cell)
println(indexPath)
}
}
Use didSelectRowAtIndexPath delegate method.

Getting UITableViewCell from UITapGestureRecognizer

I have a UITableView with multiple sections and in my cellForRowAtIndexPath method I add a UITapGestureRecognizer to the cell's imageView (each cell has a small image). I have been successful in accessing that image (in order to change it) by using:
var imageView : UIImageView! = sender.view! as UIImageView
What I need to do now, however, is access the cell's data, which I believe means I need to be able to access the cell in order to get the section and row number. Can anyone advise on how to do this? The idea is that I am changing the image to an unchecked checkbox if the task is done, but then in my data I need to actually mark that task as done.
In your function that handles the tap gesture recognizer, use tableView's indexPathForRowAtPoint function to obtain and optional index path of your touch event like so:
func handleTap(sender: UITapGestureRecognizer) {
let touch = sender.locationInView(tableView)
if let indexPath = tableView.indexPathForRowAtPoint(touch) {
// Access the image or the cell at this index path
}
}
From here, you can call cellForRowAtIndexPath to get the cell or any of its content, as you now have the index path of the tapped cell.
You can get the position of the image tapped in order to find the indexPath and from there find the cell that has that image:
var position: CGPoint = sender.locationInView(self.tableView)
var indexPath: NSIndexPath = self.tableView.indexPathForRowAtPoint(position)!
var cell = self.tableView(tableView, cellForRowAtIndexPath: indexPath)
You're missing that you can use the Delegate design pattern here.
Create a protocol that tells delegates the state changes in your checkbox (imageView):
enum CheckboxState: Int {
case .Checked = 1
case .Unchecked = 0
}
protocol MyTableViewCellDelegate {
func tableViewCell(tableViewCell: MyTableViewCell, didChangeCheckboxToState state: CheckboxState)
}
And assuming you have a UITableViewCell subclass:
class MyTableViewCell: UITableViewCell {
var delegate: MyTableViewCellDelegate?
func viewDidLoad() {
// Other setup here...
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: "didTapImage:")
imageView.addGestureRecognizer(tapGestureRecognizer)
imageView.userInteractionEnabled = true
}
func didTapImage(sender: AnyObject) {
// Set checked/unchecked state here
var newState: CheckboxState = .Checked // depends on whether the image shows checked or unchecked
// You pass in self as the tableViewCell so we can access it in the delegate method
delegate?.tableViewCell(self, didChangeCheckboxToState: newState)
}
}
And in your UITableViewController subclass:
class MyTableViewController: UITableViewController, MyTableViewCellDelegate {
// Other methods here (viewDidLoad, viewDidAppear, etc)...
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("YourIdentifier", forIndexPath: indexPath) as MyTableViewCell
// Other cell setup here...
// Assign this view controller as our MyTableViewCellDelegate
cell.delegate = self
return cell
}
override func tableViewCell(tableViewCell: MyTableViewCell, didChangeCheckboxToState state: CheckboxState) {
// You can now access your table view cell here as tableViewCell
}
}
Hope this helps!
Get tableView's cell location on a specific cell. call a function which holds the gesture of cell's Objects Like UIImage or etc.
let location = gesture.location(in: tableView)
let indexPath = tableView.indexPathForRow(at: location)
Print(indexPath.row)
Print(indexPath.section)

Resources