How can I programmatically select UICollection vie

2019-08-23 18:14发布

问题:

I'm matching the indexPath with a pointer array against my data model to see which track is playing so that I can control audio playback using the didSelectItemAt delegate method. i.e. when a cell has been tapped it checks to see which song is playing based off the indexPath of the selected cell. (I am playing and pausing the audio within the didSelectItemAt delegate based off those parameters). I do this by switching a boolean in the pointer array for the track at the given indexPath.

This works great for playing and pausing the audio when you are manually selecting the cells (tapping or clicking).

I would like to select other cells programmatically however when I do so it seems the boolean is not set correctly.

Here is some code to help explain:

  let workData = TypeData.createWorkMusicArray()
  var musicDataArray: [TypeData] = []
  var keys = [TypeData]()
  var pointerArray: [TypeData : Bool]! 
  var currentTrack: Int!

    override func viewDidLoad() {
      super.viewDidLoad()

    musicDataArray = workData

    keys = [workData[0], workData[1], workData[2]]
    pointerArray = [workData[0] : false, workData[1] : false, workData[2] : false]

    musicCollectionView.allowsMultipleSelection = false

}


 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

    currentTrack = indexPath.item

    if pointerArray[keys[indexPath.item]] == false {
        pointerArray[keys[indexPath.item]] = true
        print("Playing", keys[indexPath.item])
        playAudio()
        return
    }

    pointerArray[keys[indexPath.item]] = false
    print("Stop", keys[indexPath.item])
    pause()
}

Also note that I do not use didDeselectItemAt delegate method for any of playback logic in my app as I only have one AVPlayer and a new item is passed into when the cell is selected (The deselect method here just sets the boolean to false so I can change the UI)

   func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {

pointerArray[keys[indexPath.item]] = false
print("Stop", keys[indexPath.item])

}

This is the function I am using to try and programatically select the cells:

func previousTrack() {
    func getCurrentTrack() {
        if currentTrack - 1 < 0 {
            currentTrack = (musicDataArray.count - 1) < 0 ? 0 : (musicDataArray.count - 1)

        } else {
            currentTrack -= 1

        }
    }

    getCurrentTrack()

    //I've tried setting the boolean here to false but it still does not work 
    pointerArray[keys[currentTrack]] = false

    self.musicCollectionView.selectItem(at: IndexPath(item: currentTrack, section: 0), animated: true, scrollPosition: .top)
    self.musicCollectionView.delegate?.collectionView!(self.musicCollectionView, didSelectItemAt: IndexPath(item: currentTrack, section: 0))
}

  @IBAction func rewindBtnTapped(_ sender: Any) {
  previousTrack()
}

When rewindBtnTapped is called it will select the previous cell but then when I decide to select/tap/click on a cell the behaviour isn't consistent i.e. the booleans that enable the playing and pausing have been mixed up.

I really appreciate your time - Thank you

回答1:

The code I posted before was just to show you a little bit about the logic of having a variable that tells you if the song is playing or not. But it is not a good production code, it was mostly for illustration purposes (it is not easy to maintain a dictionary and a pointer array). So, I'm gonna try to guide you in a different way so that your app becomes simpler to read later and you detach the function of playing as much as possible from your controller -> Reason: Imagine that later you would like to reuse the same cell to populate a collection view that displays the songs that correspond to an Album for example. Then you will have to refactor all the code to make your new collection view work as well. However, the point of any object oriented programming is that you can actually build "Objects" that can be reused as you may need.

So, first think what exactly your "song" object will have to be. Most probably is not only gonna be a path to a file that gets played anytime a user taps a cell from your collection view. I guess a song will also have an image, length, name, album, etc etc. If you continue creating arrays for each single variable that a song will need to display, you will probably end up with countless arrays that need to be maintained separately. That is not so easy and is surely more prone to fails.

The first thing the will be to create a class or an struct that actually contains all the data of you song. For example (using struct - you can use class if you want):

struct Song {

    var songName : String
    var isPlaying : Bool = false

    init(songName : String) {     
        self.songName = songName     
    }
}

I'm only using two variables for the song object but you can add as many as you want. Just pay attention to which variable can be initialized when you create an instance of your "Song" object. In the example only the song name can be initialized, the Bool for playing is initialized by default to false and is the variable that tells you if the song is playing or not.

Then you can setup you Cell, which in this case will be the responsible for setting all the views depending on your "Song". For this you can create a custom class for the cell, in this case I just called it Cell, you can call it whatever you want.

class Cell : UICollectionViewCell {

    var song : Song?

    weak var cellDelegate : CellDelegate?

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupViews()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupViews() {
        self.backgroundColor = .red
    }

    func play() { 
        if !(song?.isPlaying)! {
            song?.isPlaying = true
            print("Playing", (song?.songName)!)
            return
        }
        song?.isPlaying = false
        print("Stop", (song?.songName)!)
    }

}

Notice that cell has play() function and a variable called song var song : Song?. The reason is that I am going to let each cell decide when to play or pause. This detaches the cell from the play function that you would use in collection view.

Now that you have a "Song" object you can easily create an array of songs in your view controller or any other class. For example:

var songArray : [Song] = [Song.init(songName: "Song One"), Song.init(songName: "Song Two"), Song.init(songName: "Song Three")]

Finally you can initialize each of the cells of the collection view with each of the songs in your song array. For that you will use the cellForItemAt function:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! Cell
        cell.song = songArray[indexPath.item]
        cell.cellDelegate = self
        return cell
    }

Now, you will see that the cell also has a weak var called var cellDelegate : CellDelegate this is the protocol that you will use to control the cell play and pause function. You can read further about delegates later and how they help to detach your cell from the controller as much as possible. Also, another collection view controller may conform to this delegate and you will have the same access to the function of the cell without having to rewrite all the code.

The protocol can be set outside of your cell class:

protocol CellDelegate : class {
    func didSelectCell (for cell: Cell)
}

Finally, conform the view controller to CellDelegate:

class ViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, CellDelegate

And now the code for playing, pausing, previous, next, etc etc becomes way simpler and cleaner

 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        currentIndexPathItem = indexPath.item
        let cellToPlay = collectionView.cellForItem(at: IndexPath(item: currentIndexPathItem, section: 0)) as! Cell
        didSelectCell(for: cellToPlay)
    }

    func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
        let cellToStop = collectionView.cellForItem(at: indexPath) as! Cell
        if (cellToStop.song?.isPlaying)! { didSelectCell(for: cellToStop) }
    }

    @objc func previous () {
        let playingCell = collectionView.cellForItem(at: IndexPath(item: currentIndexPathItem, section: 0)) as! Cell
        if (playingCell.song?.isPlaying)! { didSelectCell(for: playingCell) }
        if currentIndexPathItem - 1 < 0 { return }
        else { currentIndexPathItem = currentIndexPathItem - 1 }
        let cellToPlay = collectionView.cellForItem(at: IndexPath(item: currentIndexPathItem, section: 0)) as! Cell
        didSelectCell(for: cellToPlay)
    }

    func didSelectCell(for cell: Cell) {
        cell.play()
    }

Notice the didSelectCell function, that is all you need to add to conform to the delegate and play or pause your song. And that is it. Much cleaner and much simpler to code.