My scenario, I am recording audio and save into Coredata then listing into tableView with customcell play/pause single button. I cant able to do below things
Detect end of playback and change audio play cell button image pause to play
While first row cell audio playing time, If user click second row play button need to stop first row cell audio play and change image pause to play
then second row cell button play to pause
How to do above two operations?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "Cell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! CustomCell
// Configure the cell...
cell.likeBtn.addTarget(self, action: #selector(TableViewController.liked), for: .touchUpInside)
return cell
}
#objc func liked(sender: UIButton) {
// Here I need to change button icon and handle row
}
Is kind of difficult to explain without knowing more about the rest of the app. It's important to give a little more context. I'll try to give you an answer assuming certain things.
First I'll asume you have Song objects that looks like this:
public struct Song: Equatable {
let name: String
let fileName: String
}
Your database has the following public methods and property:
class DB {
func getSong(_ position: Int) -> Song?
func getPosition(_ song: Song) -> Int?
var count: Int
}
To make it easy for this sample code on the init some predefine data is initialize.
Also there is a Player object that manages playing audio with the following public methods:
class Player {
func currentlyPlaying() -> Song?
func play(this song: Song)
func stop()
}
Now with this previously defined I created a custom cell to display the name and a button for each Song in the database. The definition is this:
class CustomCell : UITableViewCell {
#IBOutlet weak var label: UILabel!
#IBOutlet weak var button: UIButton!
}
And looks like:
Next let's define the tableview's datasource methods. In each cell a target is added for each button's touchUpInside event (as you defined it in the question).
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return DB.shared.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell",
for: indexPath) as! CustomCell
cell.label.text = DB.shared.getSong(indexPath.row)!.name
cell.button.addTarget(self, action: #selector(buttonTapped(_:)),
for: .touchUpInside)
return cell
}
Next let's define a helper method to locate a UIVIew inside a TableView. With this method we can get the IndexPath of any control inside any cell in a TableView. The return value is optional in order to return nil if not found.
func getViewIndexInTableView(tableView: UITableView, view: UIView) -> IndexPath? {
let pos = view.convert(CGPoint.zero, to: tableView)
return tableView.indexPathForRow(at: pos)
}
Another helper method was define to change the image of a button with an animation:
func changeButtonImage(_ button: UIButton, play: Bool) {
UIView.transition(with: button, duration: 0.4,
options: .transitionCrossDissolve, animations: {
button.setImage(UIImage(named: play ? "Play" : "Stop"), for: .normal)
}, completion: nil
}
A method was necessary to stop any current playing song. The first thing is to check wether a song is playing, if so call Player's stop method. Then let's localize the position of the Song in the database, which in my case corresponds to the position in the TableView; having this let's create a IndexPath to get the corresponding cell, finally call changeButtonImage with the cell's button to change the image.
func stopCurrentlyPlaying() {
if let currentSong = Player.shared.currentlyPlaying() {
Player.shared.stop()
if let indexStop = DB.shared.getPosition(currentSong) {
let cell = tableView.cellForRow(at: IndexPath(item: indexStop, section: 0)) as! CustomCell
changeButtonImage(cell.button, play: true)
}
}
}
The buttonTapped method which starts playing a song have some logic inside. First the method signature needs #objc in order to be used in the addTarget method. The logic is a follows:
Localize the button's IndexPath in the table view using the helper method.
Localize the song in the database, the row number in the table corresponds to the order in the database.
If there is a song currently playing and is the same as the one localize for the button tapped it means we just want to stop the song, so stopCurrentlyPlaying is called and the method returns.
If is not the same song or nothing is playing let's call: stopCurrentlyPlaying, start playing the new song and change the tapped button's image to a Stop image.
The code looks like this:
#objc func buttonTapped(_ sender: UIButton) {
// Let's localize the index of the button using a helper method
// and also localize the Song i the database
if let index = getViewIndexInTableView(tableView: tableView, view: sender),
let song = DB.shared.getSong(index.row) {
// If a the song located is the same it's currently playing just stop
// playing it and return.
guard song != Player.shared.currentlyPlaying() else {
stopCurrentlyPlaying()
return
}
// Stop any playing song if necessary
stopCurrentlyPlaying()
// Start playing the tapped song
Player.shared.play(this: song)
// Change the tapped button to a Stop image
changeButtonImage(sender, play: false)
}
}
Here is a little video of the sample app working: sample app
Related
I have TableViewController and AudioPlayerViewController. In TableViewController I want to know how many tracks have been listened to. And show the image in the listened cells TableViewController. To detect that track have been listened I use this code in AudioPlayerViewController:
func audioPlayerDidFinishPlaying(_ audioPlayer: AVAudioPlayer, successfully flag: Bool) {
UserDefaults.standard.set( arrayOfListened(trackIndex), forKey: "key")
}
And to show image in listened cells in TableViewController next:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: String(format: "cell", indexPath.row), for: indexPath) as! MasterViewCell
if indexPath.row == arrayOfListened[indexPath.row] {
cell.cellStatusImage.image = UIImage(named: "statusDone.png")
}
}
But my app crashed. How to fix it?
The problem is that you are trying to get an element from the array by indexPath.row which probably doesn't exist. Try to use something like this:
...
guard
arrayOfListened.indices.contains(indexPath.row),
indexPath.row == arrayOfListened[indexPath.row]
else {
return cell
}
... your cell configuration
Or, if your arrayOfListened just contains song IDs, you can simply use:
if arrayOfListened.contains(indexPath.row) {
... your cell configuration
}
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 am trying to implement play/pause button in tableview cell. each cell having single button, whenever user click it, It should change button image also need to call required function, also after scroll it should same.
Below code I am using
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) - > UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("productCell") as ? SepetCell
cell.onButtonTapped = {
//Do whatever you want to do when the button is tapped here
}
See first of all every button of the tableView Cell will have a unique tag associated with it, so in order to update the button of a particular cell, you will have to define the tag of a button in the cells and then pass this tag to your function to perform action on that particular button of the selected cell.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = tableView.dequeueReusableCell(withIdentifier: "cell_identifier", for:
indexPath) as! CellClass
cell.button.tag = indexPath.row
cell.button.addTarget(self, action: #selector(playpause), for: .touchUpInside)
}
#objc func playpause(btn : UIButton){
if btn.currentImage == UIImage(named: "Play") {
btn.setImage(UIImage(named : "Pause"), forState: .Normal)
}
else {
btn.setImage(UIImage(named : "Play"), forState: .Normal)
}
// perform your desired action of the button over here
}
State of the art in Swift are callback closures. They are easy to implement and very efficient.
In the data source model add a property
var isPlaying = false
In Interface Builder select the button in the custom cell and press ⌥⌘4 to go to the Attributes Inspector. In the popup menu State Config select Default and choose the appropriate image from Image popup, Do the same for the Selected state.
In the custom cell add a callback property and an outlet and action for the button (connect both to the button). The image is set via the isSelected property.
#IBOutlet weak var button : UIButton!
var callback : (()->())?
#IBAction func push(_ sender: UIButton) {
callback?()
}
In the controller in cellForRow add the callback, item is the current item of the data source array. The state of the button is kept in isPlaying
cell.button.isSelected = item.isPlaying
cell.callback = {
item.isPlaying = !item.isPlaying
cell.button.isSelected = item.isPlaying
}
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
}
}
}
I have a UISwitch in a tableviewcontroller, and when the switch is toggled I want it to change the value of a boolean variable in an array I created inside the view controller, that the cell is related to. Kind of like the Stock Alarm App on IOS, where each cell has a UISwitch, and toggling the switch will turn off each individual alarm. So with the UISwitch, with its selector code, this is inside the cellForRowAtIndexPath method
//switch
let lightSwitch = UISwitch(frame: CGRectZero) as UISwitch
lightSwitch.on = false
lightSwitch.addTarget(self, action: #selector(switchTriggered), forControlEvents: .ValueChanged)
//lightSwitch.addTarget(self, action: "switchTriggered", forControlEvents: .ValueChanged )
cell.accessoryView = lightSwitch
I want it to do this
func switchTriggered(a: Int) {
changeValueOfArray = array[indexPath.row]
}
I don't have the code written for that part yet, but my question is, How can i let the switchTriggered function see the indexPath.row value, without passing it as an argument to the function because I can't because its a selector?
let lightSwitch = UISwitch(frame: CGRectZero) as UISwitch
lightSwitch.on = false
lightSwitch.addTarget(self, action: #selector(switchTriggered), forControlEvents: .ValueChanged)
lightSwitch.tag = indexpath.row
cell.accessoryView = lightSwitch
Let save your boolean value in Array
func switchTriggered(sender: UISwitch) {
sender.on ? array[sender.tag]=1 : array[sender.tag]=0
}
}
The basic idea is that you can capture the cell for which the switch was flipped and then use tableView.indexPath(for:) to translate that UITableViewCell reference into a NSIndexPath, and you can use its row to identify which row in your model structure needs to be updated.
The constituent elements of this consist of:
Create a model object that captures the information to be shown in the table view. For example, let's imagine that every cell contains a name of a Room and a boolean reflecting whether the light is on:
struct Room {
var name: String
var lightsOn: Bool
}
Then the table view controller would have an array of those:
var rooms: [Room]!
I'd define a UITableViewCell subclass with outlets for the label and the switch. I'd also hook up the "value changed" for the light switch to a method in that cell. I'd also set up a protocol for the cell to inform its table view controller that the light switch was flipped:
protocol RoomLightDelegate: class {
func didFlipSwitch(for cell: UITableViewCell, value: Bool)
}
class RoomCell: UITableViewCell {
weak var delegate: RoomLightDelegate?
#IBOutlet weak var roomNameLabel: UILabel!
#IBOutlet weak var lightSwitch: UISwitch!
#IBAction func didChangeValue(_ sender: UISwitch) {
delegate?.didFlipSwitch(for: self, value: sender.isOn)
}
}
I'd obviously set the base class for the cell prototype to be this UITableViewCell subclass and hook up the #IBOutlet references as well as the #IBAction for the changing of the value for the switch.
I'd then have the UITableViewDataSource methods populate the cell on the basis of the Room properties:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rooms.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SwitchCell", for: indexPath) as! RoomCell
let room = rooms[indexPath.row]
cell.roomNameLabel.text = room.name
cell.lightSwitch.setOn(room.lightsOn, animated: false)
cell.delegate = self
return cell
}
Note, the above cellForRowAtIndexPath also specifies itself as the delegate for the cell, so we'd want to implement the RoomLightDelegate protocol to update our model when the light switch is flipped:
extension ViewController: RoomLightDelegate {
func didFlipSwitch(for cell: UITableViewCell, value: Bool) {
if let indexPath = tableView.indexPath(for: cell) {
rooms[indexPath.row].lightsOn = value
}
}
}
Now, I don't want you to worry about the details of the above. Instead, try to capture some of the basic ideas:
Bottom line, to your immediate question, once you know which cell was was updated, you can inquire with the UITableView to determine what NSIndexPath that UITableViewCell reference corresponds to, using tableView.indexPath(for:).
Swift 3 Update:
let lightSwitch = UISwitch(frame: CGRect.zero) as UISwitch
lightSwitch.isOn = false
lightSwitch.addTarget(self, action: #selector(switchTriggered), for: .valueChanged)
lightSwitch.tag = indexPath.row
cell?.accessoryView = lightSwitch