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
}
}
}
Related
I have a button and a label in a table view (I am using 8 rows )and for some reason when I click the first button I get indexPath nil error, but when I click the second button (2nd row) I get the first row label. When I click the 3rd row button, I get the second row label etc. Why are they misaligned. I want when I click the first row button to get the first row label etc. Please see the code below. Thank you !!
#objc func btnAction(_ sender: AnyObject) {
var position: CGPoint = sender.convert(.zero, to: self.table)
print (position)
let indexPath = self.table.indexPathForRow(at: position)
print (indexPath?.row)
let cell: UITableViewCell = table.cellForRow(at: indexPath!)! as
UITableViewCell
print (indexPath?.row)
print (currentAnimalArray[(indexPath?.row)!].name)
GlobalVariable.addedExercises.append(currentAnimalArray[(indexPath?.row)!].name)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as? TableCell else {return UITableViewCell() }
// print(indexPath)
cell.nameLbl.text=currentAnimalArray[indexPath.row].name
// print("\(#function) --- section = \(indexPath.section), row = \(indexPath.row)")
// print (currentAnimalArray[indexPath.row].name)
cell.b.tag = indexPath.row
// print (indexPath.row)
cell.b.addTarget(self, action: #selector(SecondVC.btnAction(_:)), for: .touchUpInside)
return cell
}
Frame math is a worst-case scenario if you have no choice. Here you have a lot of choices.
For example why don't you use the tag you assigned to the button?
#objc func btnAction(_ sender: UIButton) {
GlobalVariable.addedExercises.append(currentAnimalArray[sender.tag].name)
}
A swiftier and more efficient solution is a callback closure:
In TableCell add the button action and a callback property. The outlet is not needed. Disconnect the outlet and connect the button to the action in Interface Builder. When the button is tapped the callback is called.
class TableCell: UITableViewCell {
// #IBOutlet var b : UIButton!
#IBOutlet var nameLbl : UILabel!
var callback : (()->())?
#IBAction func btnAction(_ sender: UIButton) {
callback?()
}
}
Remove the button action in the controller.
In cellForRow assign a closure to the callback property
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// no guard, the code must not crash. If it does you made a design mistake
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! TableCell
let animal = currentAnimalArray[indexPath.row]
cell.nameLbl.text = animal.name
cell.callback = {
GlobalVariable.addedExercises.append(animal.name)
}
return cell
}
You see the index path is actually not needed at all. The animal object is captured in the closure.
You already pass indexPath.row with button tag. Use the tag as index simply
#objc func btnAction(_ sender: UIButton) {
GlobalVariable.addedExercises.append(currentAnimalArray[sender.tag].name)
}
I have a tableview, and in each cell I have one button called drop down. So when user presses any option in my drop down - the hidden elements like one more drop down, one name label, one save button will be visible. So again when user presses my save button again those elements will be hidden. Now the issues is when I select my button in two or three cells and if I scroll up and down automatically which and all cell showing the elements that and all getting hide. I need to show which and all cell is clicked and showed the elements.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CartDetailsCell", for: indexPath) as! CartDetailsCell
cell.selectionStyle = UITableViewCellSelectionStyle.none
let notClicked = !selectedIndexPaths.contains(indexPath)
print(notClicked)
cell.noOfQtyOuterView.isHidden = notClicked
cell.saveDataButnOtlet.isHidden = notClicked
cell.noOfQtyButnOutlet.isHidden = notClicked
}
#IBAction func dropDownButnClick(_ sender: Any) {
guard let button = sender as? UIButton else {
return
}
let indexPath = IndexPath(item: button.tag, section: 0)
let cell = self.tableView.cellForRow(at: indexPath) as! CartDetailsCell
dropDown.anchorView = button
dropDown.dataSource = ["Edit", "Cancel"]
dropDown.selectionAction = { [unowned self] (index: Int, item: String) in
switch index {
case 0:
cell.noOfQtyOuterView.isHidden = false
cell.saveDataButnOtlet.isHidden = false
cell.noOfComboOuterViewButn.isHidden = false
case 2:
}
}
Once the button is hidden it will never be un-hidden until you explicitly make it unhidden.
"Now the issues is when i select my button in two or three cells and if i scroll up and down automatically which and all cell showing the elements that and al getting hide"
let cell = tableView.dequeueReusableCell(withIdentifier: "CartDetailsCell", for: indexPath) as! CartDetailsCell
As you are using the cell with the hidden button is reused, it will make the button remain hidden for remaining cells
I suggest to use following pattern, will save you time and you'll have a more reusable and pretty code:
protocol CartDetailsCellDelegate: class {
func didTouchDropDownButton(in cell: CartDetailsCell)
....
}
final class CartDetailsCell: UITableViewCell {
....
weak var delegate: CartDetailsCellDelegate?
#IBAction func didTouchDropDownButton(_ sender: UIButton) {
delegate?.didTouchDropDownButton(in: self)
}
...
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.delegate = self
...
}
extension ViewController: CartDetailsCellDelegate {
func didTouchDropDownButton(in cell: CartDetailsCell) {
// Do your stuff here, you have the cell, don't have to play with tags
}
}
timer.gif
↑↑↑↑↑↑↑↑↑ I show the problem in this GIF ↑↑↑↑↑
There is a timer and stratButton in the tableView cell, when I click the Button, the timeLabel will start running.
Now the problem is when I scroll the running timer out of the screen and scroll it back, the timer will reset and stop.
Hope someone can help me! I check many solution but they didn't work for me! I am almost cry.
my cellForRow:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! TimerTableViewCell
cell.timer?.invalidate()
let item = myTimerList[indexPath.row]
cell.timerName.text = item.timerName
cell.secondLeftLabel.text = "\(item.timerSecond)"
return cell
}
I created a small project contained my code for you to modify : https://app.box.com/s/axluqkjg0f7zjyigdmx1lau0c9m3ka47
You need to preserve the state of each cell
class Service {
static let shared = Service()
var myTimerList = [TimerClass]()
}
//
here i added another 2 vars , why timerName is left despite you have to init them the same because current will hold the changing value
class TimerClass {
let timerSecond : Int
let timerName : String
var current: Int
var isPlaying = false
init(second:Int, name:String) {
timerSecond = second
timerName = name
current = second
}
}
//
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "myCell", for: indexPath) as! TimerTableViewCell
let item = Service.shared.myTimerList[indexPath.row]
cell.tag = indexPath.row // to access the array inside the cell
if item.isPlaying {
cell.play() // add play method to the cell it has same button play action
}
else {
cell.timer?.invalidate()
}
cell.timerName.text = item.timerName
cell.secondLeftLabel.text = "\(item.current)"
return cell
}
//
inside the cell when the button is clicked , change isPlaying property to true and to false when stopped like this
// here self is the cell itself
Service.shared.myTimerList[self.tag].isPlaying = true
also when the timer ticks change
Service.shared.myTimerList[self.tag].current = // value
You are reusing UITableViewCell. It means once cell is out of screen, it will be reused for the cell getting into the screen. Whenever it happens, it calls the delegate method - means cell.timer?.invalidate() is invoked again and again.
Remove cell.timer?.invalidate() from cellForRowAt method, because every time you scroll the tableView this method is called to display new cells. In your case when you have started a timer for a cell and that cell is not in the view, next time you scroll to bring that cell again to the view cell.timer?.invalidate() causes it to stop.
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
}
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