How to handle tableView cell audio play and pause button using Swift?

Viewed 2932

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

  1. Detect end of playback and change audio play cell button image pause to play
  2. 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
}
1 Answers

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:

Prototype custom cell

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:

  1. Localize the button's IndexPath in the table view using the helper method.
  2. Localize the song in the database, the row number in the table corresponds to the order in the database.
  3. 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.
  4. 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