Activity indicator view in table cell - swift

I have a table, I want to make it so that when I click on a cell, the activity indicator spins around it, and not somewhere in an incomprehensible place
It looks like this. In the code, I have the MainViewController module and the Presenter module which determines when to start the activity indicator. I have an outlet
#IBOutlet weak var activity: UIActivityIndicatorView! = UIActivityIndicatorView(style: .gray)
I have two functions in the view controller that start the animation and stop
func startActivity() {
activity.startAnimating()
}
func stopActivity() {
activity.stopAnimating()
}
there is a function that handles a click on a cell
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
output.callFromThePresenter(array: array[indexPath.row])
}
This function is implemented in the presenter.
func callFromThePresenter(array: String) {
let userInteractiveQueue = DispatchQueue.global(qos: .userInteractive)
async {
DispatchQueue.main.async {
self.view.startActivity()
}
userInteractiveQueue.async {
self.interactor.functionFromInteractor(data: array)
}
}
}
as I assumed, in the view controller, when you click on a cell, the callFromThePresenter () function will work, and the animation function and the data transfer function in the Interactor will start in it, as soon as the Interactor has completed this function, it will return the data to the Presenter and inside the call back function, I will run the stopActivity () function. And it worked until I decided to set the positioning
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
output.callFromThePresenter(array: array[indexPath.row])
let cell = tableView.cellForRow(at: indexPath)
cell?.accessoryView = self.activity
}
as soon as i added these two lines
let cell = tableView.cellForRow(at: indexPath)
cell?.accessoryView = self.activity
I broke everything, as if I started up, I click on the table cell and the wheel turns where it is needed and everything works, but after receiving the result, I poke at some cell again, the whole program hangs, and I don’t know for what reason . At the same time, the functions work out how much I can understand it, because it comes to the console that the function was launched and got some result, but then the whole UI hangs tight and I can't do anything

Simple example:
In Interface Builder set the Style of the table view cell to custom.
Drag a label and an activity indicator into the cell and set appropriate constraints if needed.
Select the activity indicator, press ⌘⌥4 (Attributes Inspector) and check Hides When Stopped.
Create a custom class, a subclass of UITableViewCell in the project
class TitleCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var activity: UIActivityIndicatorView!
var isRunning : Bool = false {
didSet {
isRunning ? activity.startAnimating() : activity.stopAnimating()
}
}
}
Set the class of the prototype cell in Interface Builder to TitleCell and connect the label and the indicator to the IBOutlets
Create a data model with at least a title and a isRunning property.
struct Model {
let title: String
var isRunning : Bool
}
Declare a data source array
var items = [Model]()
In cellForRow update the UI elements (replace the identifier with your real identifier)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! TitleCell
let item = items[indexPath.row]
cell.titleLabel!.text = item.title
cell.isRunning = item.isRunning
return cell
}
To start and stop the indicator implement didSelectRow
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: false)
items[indexPath.row].isRunning.toggle()
tableView.reloadRows(at: [indexPath], with: .none)
}
To change the state of the indicator use always the pattern to set isRunning in the model and reload the row.

Related

Did select show check/image using custom tableview cell

Ok so this seems like a really simple question, but I can't seem to solve it efficiently or find anything on the web that has the correct answer. So here goes.
I have a tableViewController with a prototype cell running off a custom class. Inside the prototype cell I have an image of a checkmark which I want to display when the user selects the cell and hide the image when another cell gets selected. I've got an approach working but it requires me to reloadData on the table which seems really inefficient. So there must be a better way right?
My code...
//CUSTOM CLASS (trimmed code down to just show relevant code)
class GoalsTableViewCell: UITableViewCell {
#IBOutlet weak var gNameLabel: UILabel!
#IBOutlet weak var gIsSelectedImage: UIImageView!
}
}
//TABLE VIEW CONTROLLER (trimmed it down to just show relevant code)
class GoalsTableVC: UITableViewController {
var selectedGoalId = ""
var selectedInd = 0
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let gCell = tableView.dequeueReusableCell(withIdentifier: "gCell", for: indexPath) as! GoalsTableViewCell
gCell.gNameLabel.text = goalsData[indexPath.row].gName
if indexPath.row == selectedInd {
gCell.gIsSelectedImage.isHidden = false
} else {
gCell.gIsSelectedImage.isHidden = true
}
return gCell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedGoalId = goalsData[indexPath.row].id!
selectedInd = indexPath.row
tableView.reloadData()
}
}
It's possible to reload only the affected rows
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedGoalId = goalsData[indexPath.row].id!
let indexPathsToReload = [indexPath, IndexPath(row: selectedInd, section: 0)]
selectedInd = indexPath.row
tableView.reloadRows(at: indexPathsToReload, with: .automatic)
}

UITextField can't become first responder in UITableView

Context
Basic list. When user press '+', code creates a new item with default text that user can change.
Problem
I want to focus the new item as soon as user press '+' so that user can type desired name. I try to achieve this with this code:
func focus() {
title.becomeFirstResponder()
title.selectAll(nil)
}
But becomeFirstResponder() always returns false.
How can I give focus to UITextField in my UITableviewCell after creation ?
Here is full code of UITableViewController for reference:
import UIKit
class ViewController: UITableViewController{
var model: [String] = ["Existing item"]
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return model.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemUI", for: indexPath) as! ItemUI
cell.update(with: model[indexPath.item])
return cell
}
#IBAction func createItem(_ sender: UIBarButtonItem) {
let indexPath = IndexPath(item: model.count, section: 0)
model.append("new item")
tableView.reloadData()
tableView.scrollToRow(at: indexPath, at: .top, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
let cell = self.tableView(self.tableView, cellForRowAt: indexPath) as! ItemUI
cell.focus()
}
}
}
class ItemUI: UITableViewCell{
#IBOutlet var title: UITextField!
func update(with: String) {
title.text = with
}
func focus() {
title.becomeFirstResponder()
title.selectAll(nil)
}
}
Ok I found the problem!
I was using method self.tableView(self.tableView, cellForRowAt: indexPath) instead of self.tableView.cellForRow(at: indexPath).
Here is what the documentation has to say:
"Never call this method yourself. If you want to retrieve cells from your table, call the table view's cellForRow(at:) method instead."
You can add a boolean in your view controller to keep track of adding item i.e isAddingItem with default value false and when you add new item simply update isAddingItem to true. In tableview cellForRowAt method check for last cell of tableview and if isAddingItem is true then selected all text of textfield and make it first responder.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ItemUI", for: indexPath) as! ItemUI
cell.update(with: model[indexPath.item])
if isAddingItem && indexPath.item == model.count {
// make your textfield first here and select text here
}
return cell
}
Also check if you have set textfield delegate.

Update Specific Button Image in UITableView Cell

I am trying to add an action to my "like" button. So that when the user taps the heart UIButton in a cell, the heart in the cell they tapped updates to a pink heart showing that they liked it. But instead it likes the heart they tapped and another random heart in a different cell that they did not interact with. I have been on this all day and any help would be grateful. For Example, if I like/tap my heart UIButton the buttons image I tapped updates, but when I scroll down another random heart updates from that same first cell button tap.
Also When I scroll and the cell leaves view and scroll back up the image returns back to unlike and other like buttons become liked.
Keep a data model for your buttons state
Try with the below code
struct TableModel {
var isLiked: Bool
}
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var dataSource: [TableModel] = []
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
dataSource = Array(repeating: TableModel(isLiked: false), count: 20)
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.showsVerticalScrollIndicator = false
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
#objc func buttonSelected(_ sender: UIButton) {
dataSource[sender.tag].isLiked = !dataSource[sender.tag].isLiked
let indexPath = IndexPath(row: sender.tag, section: 0)
tableView.reloadRows(at: [indexPath], with: .automatic)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.likeBtn.tag = indexPath.row
cell.likeBtn.addTarget(self, action: #selector(buttonSelected(_:)), for: .touchUpInside)
let isLiked = dataSource[indexPath.row].isLiked
if isLiked {
cell.likeBtn.setImage(UIImage(named: "liked"), for: UIControl.State.normal)
} else {
//set unlike image
}
return cell
}
}
Currently, you have a hardcoded number of rows, but anyway you will need to have a data source with data models. When you press the button, you have to save the state of the button of a specific row. I would recommend you create a model first.
Here I provided an easy (but flexible enough) way how to do this. I haven't debugged it, but it should work and you can see the idea. I hope this would be helpful.
Create Cell Model
struct CellViewModel {
let title: String
var isLiked: Bool
// Add other properties you need for the cell, image, etc.
}
Update cell class
It's better to handle top action right in the cell class. To handle this action on the controller you can closure or delegate like I did.
// Create a delegate protocol
protocol TableViewCellDelegate: AnyObject {
func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell)
}
class TableViewCell: UITableViewCell {
// add a delegate property
weak var delegate: TableViewCellDelegate?
#IBOutlet var titleTxt: UILabel!
#IBOutlet var likeBtn: UIButton!
//...
override func awakeFromNib() {
super.awakeFromNib()
// You can add target here or an action in the Storyboard/Xib
likeBtn.addTarget(self, action: #selector(likeButtonSelected), for: .touchUpInside)
}
/// Method to update state of the cell
func update(with model: CellViewModel) {
titleTxt.text = model.title
likeBtn.isSelected = model.isLiked
// To use `isSelected` you need to set different images for normal state and for selected state
}
#objc private func likeButtonSelected(_ sender: UIButton) {
sender.isSelected.toggle()
delegate?.didSelectLikeButton(isLiked: sender.isSelected, forCell: self)
}
}
Add an array of models and use it
This is an updated class of ViewController with usage of models.
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
// Provide a list of all models (cells)
private var cellModels: [CellViewModel] = [
CellViewModel(title: "Title 1", isLiked: false),
CellViewModel(title: "Title 2", isLiked: true),
CellViewModel(title: "Title 3", isLiked: false)
]
#IBOutlet var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
overrideUserInterfaceStyle = .light
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.showsVerticalScrollIndicator = false
self.tableView.reloadData()
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
// return count of cell models
return cellModels.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
let model = cellModels[indexPath.row]
// call a single method to update the cell UI
cell.update(with: model)
// and you need to set delegate in order to handle the like button selection
cell.delegate = self
return cell
}
}
extension ViewController: TableViewCellDelegate {
func didSelectLikeButton(isLiked: Bool, forCell cell: TableViewCell) {
// get an indexPath of the cell which call this method
guard let indexPath = tableView.indexPath(for: cell) else {
return
}
// get the model by row
var model = cellModels[indexPath.row]
// save the updated state of the button into the cell model
model.isLiked = isLiked
// and set the model back to the array, since we use struct
cellModels[indexPath.row] = model
}
}

How to keep an object's reference when using UITableView?

The problem I'm facing is probably some lack of understanding on the concept of reusable cells. I have, let's say, 30 rows to be created and each of them has a UISwitch.
When I toggle one of the switches, it's behavior should affect the other 29. The point is: as far as I know, iOS doesn't create all of them at once, but rather wait to reuse the cells when the TableView is scrolled up and down.
How can I keep a copy of those reused objects and tell iOS to set the proper value to the switches?
I've thought on having the cells appended to a [UISwitch] but I can't manage to have all the 30 cells in there, look:
...
var switches = [UISwitch]()
...
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Field10Cell", for: indexPath) as! Field10TableViewCell
...
//cell.value is a UISwitch
if !switches.contains(cell.value) {
switches.append(cell.value)
}
return cell
}
You could create a set which stores the indexes of the cells whose switches have been pressed.
var activeSwitches = Set<IndexPath>()
Whenever a user presses the switch on a cell, you store it on the set like this:
activeSwitches.insert(indexPath)
If you need to check if a switch was activated, just check if its container cell's indexPath is in active switches like so:
if activeSwitches.contains(indexPath) {
// do something
}
In order to know when a user pressed a specific switch I recommend the folliwing:
In cellForRowAtIndexPath save the current indexPath into your Field10TableViewCell.
Create a protocol on Field10TableViewCell and add a delegate.
protocol Field10Delegate {
func didChangeSwitch(value: Bool, indexPath: IndexPath)
}
class Field10TableViewCell {
var delegate: Field10Delegate?
var indexPath: IndexPath?
#IBOutlet weak var fieldSwitch: UISwitch! // Can't use 'switch' as a variable name
#IBAction func switchValueChanged(_ sender: UISwitch) {
if let indexPath = indexPath {
delegate?.didChangeSwitch(value: sender.isOn, indexPath: indexPath)
}
}
When you create a cell, set the view controller as a delegate
let cell = tableView.dequeueReusableCell(withIdentifier: "Field10Cell", for: indexPath) as! Field10TableViewCell
cell.delegate = self
cell.indexPath = indexPath
Make your view controller comply with the protocol:
extension ViewController: Field10Delegate {
/* Whenever a switch is pressed on any cell, this delegate will
be called. This is a good place also to trigger a update to
your UI if it has to respond to switch changes.
*/
func didChangeSwitch(value: Bool, indexPath: IndexPath) {
if value {
activeSwitches.insert(indexPath)
} else {
activeSwitches.remove(indexPath)
}
updateUI()
}
}
With the above, at any point you will know which switches are active or not and you can process the dequeued cells with this information.

Issue with Swift TableViewCell: change background color for selected row

I have a strange issue with my tableView.
I have a List of audio tracks and a segue to an audio player in order to play the selected track at a specific row. Everything works fine!
I wanted to change the background color for the selected row in the table so that, once the user play the audio and come back to the list of tracks (my Table View Controller) , he can see which are the previously selected rows.
But when I run It change me the color not only for the row at index path I selected but also to the item at index path + 10.
If I select the First Row it change me the color for the row at the index: 0, 10, 20, 30...
In order to change the color of the selected cell I did the follow:
// MARK: - Navigation
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("audioPlayer", sender: tableView)
var selectedCell:UITableViewCell = tableView.cellForRowAtIndexPath(indexPath) as CustomTableViewCell
selectedCell.contentView.backgroundColor = UIColor.greenColor()
selectedCell.backgroundColor = UIColor.blueColor()
Please find a screenshot of my issue, I have selected just three rows: 1, 3, 5 but I get selected 1,3,5,11,13,15,21,23 and so on... :
https://www.dropbox.com/s/bhymu6q05l7tex7/problemaCelleColore.PNG?dl=0
For further details - if can help - here it is my Custom Table View class:
import UIKit
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var artista: UILabel!
#IBOutlet weak var brano: UILabel!
var ascoltato = false
#IBOutlet weak var labelRiproduciAscoltato: UILabel!
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
}
func setCell(artista: String, brano: String){
self.artista.text = artista
self.brano.text = brano
}
} // END MY CUSTOM TABLE VIEW CELL
Here it is the method cellForRowAtIndexPath in my TableViewController:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as CustomTableViewCell
var tracks : Brani //Brani is my custom Object for handle my tracks
cell.setCell(tracks.title!, brano: tracks.author!)
return cell
}
I am running on iPad Air with iOS 7.1.
Thank you in advance for any suggestion or advice related to my issue.
This is probably because UITableViewCells are recycled. This means the formerly selected tableViewCell gets reused by the cells at the lower indexes. This is expected behavior of a UITableView and makes sense, as it saves memory usage. To fix the issue, you will need to have your datasource keep a track of which cell is selected, and updated the cell's background color accordingly.
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
self.performSegueWithIdentifier("audioPlayer", sender: tableView)
//datasource is updated with selected state
//cell is updated with color change
}
Then in your cellForRowAtIndexPath method:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("tableCell", forIndexPath: indexPath) as CustomTableViewCell
var tracks : Brani //Brani is my custom Object for handle my tracks
cell.setCell(tracks.title!, brano: tracks.author!)
//update cell style here as well (by checking the datasource for selected or not).
return cell
}