I am developing a music app whereby users can browse from a selection of tracks and save them into a playlist. I am having an issue where if a track is saved into a playlist twice, once after the other, the play controls do not act as desired i.e. 2x track1 is added at the beginning of the playlist - if selecting 1st track1 - plays and pauses just fine, then if I select 2nd track1 (if 1st track1 has already been selected, or vice versa) it carries on playing/pausing as if it were the the 1st track1.
Is there a way to duplicate that item instead of referencing the same item? Here is some code that shows how I am populating playlists and my data structure etc.
Data Structure:
class Data: NSObject, NSCoding {
var title: String
var body: String
var colour: UIColor
var url: String
var isPlaying : Bool = false
init(title: String, body: String, colour: UIColor, url: String) {
self.title = title
self.body = body
self.colour = colour
self.url = url
}
class func createTrackArray() -> [Data] {
var array: [Data] = []
let track1 = Data(title: "Track 1 Title", body: "Track 1 Body", colour: .white, url: "track1url")
let track2 = Data(title: "Track 2 Title", body: "Track 2 Body", colour: .white, url: "track2url")
array.append(track1)
return array
}
}
Displaying Music To Browse Through:
//Global Variable
var musicDataArray: [Data] = []
class MusicVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
musicDataArray = Data.createTrackArray()
}
}
//MARK: - CollectionView Cell Configuration
extension MusicVC: UICollectionViewDelegate, UICollectionViewDataSource, CellDelegate {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return musicDataArray.count
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: .musicCell, for: indexPath) as! MusicCell
cell.cellData = musicDataArray[indexPath.item]
cell.song = musicDataArray[indexPath.item]
cell.cellDelegate = self
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
currentIndexPathItem = indexPath.item
guard let cellToPlay = musicCollectionView.cellForItem(at: IndexPath(item: currentIndexPathItem, section: 0)) as? MusicCell else {
print("Cell not here")
return
}
didSelectCell(for: cellToPlay)
prepareTrackForSavingToPlaylist(track: indexPath.item)
}
func didSelectCell(for cell: MusicCell) {
cell.play()
}
This function saves the index (and associated array item??) into a temporary array. I then send it via segue to another view controller whereby it is saved into a playlist. i.e. playlistArray will only ever have one item in it that it passes to next view controller.
func prepareTrackForSavingToPlaylist(track: Int) {
playlistArray.removeAll()
playlistArray.append(musicDataArray[track])
}
Next ViewController: Passed data (which is the information from that temporary array from the previous view controller) is then added to whatever playlist they select in the collectionview
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let passedData = Data(title: dataFromMusicVC.title, body: dataFromMusicVC.body, colour: dataFromMusicVC.colour, url: dataFromMusicVC.url)
playlistArray[indexPath.item].append(passedData)
}
And then finally the user can select a playlist and it will display all of the saved tracks in that playlist
extension UserPlaylistsVC: UICollectionViewDelegate, UICollectionViewDataSource, UserDelegate {
func didSelectCell(for cell: UserPlaylistsCell) {
cell.play()
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return playlistArray[playlistIndex].count
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let createID = "UserCell"
let userCell = collectionView.dequeueReusableCell(withReuseIdentifier: createID, for: indexPath) as! UserPlaylistsCell
//playlistIndex is just an Int that is based on which cell is selected in a different view controller so that the right playlist is accessed.
userCell.song = playlistArray[playlistIndex][indexPath.item]
userCell.cellData = playlistArray[playlistIndex][indexPath.item]
userCell.userDelegate = self
userCell.delegate = self
return userCell
}
This is the cell that is used for the tracks in the playlists:
protocol UserDelegate: class {
func didSelectCell (for cell: UserPlaylistsCell)
}
class UserPlaylistsCell: UICollectionViewCell {
@IBOutlet weak var titleLbl: UILabel!
@IBOutlet weak var bodyLbl: UILabel!
@IBOutlet weak var colourView: UIView!
var song : TypeData!
var audio = Audio()
weak var delegate: UserPlaylistDelegate?
weak var userDelegate: UserDelegate?
override func prepareForReuse() {
super.prepareForReuse()
}
override var isSelected: Bool {
didSet {
self.contentView.backgroundColor = isSelected ? UIColor.UMLightGrey : UIColor.white
}
}
override func awakeFromNib() {
super.awakeFromNib()
titleLbl.textColor = UIColor.UMDarkGrey
bodyLbl.textColor = UIColor.UMDarkGrey
colourView.layer.cornerRadius = colourView.layer.frame.size.width / 2
colourView.clipsToBounds = true
}
var cellData: TypeData! {
didSet {
titleLbl.text = cellData.title
bodyLbl.text = cellData.body
colourView.backgroundColor = cellData.colour
}
}
func play() {
if !(song?.isPlaying)! {
song?.isPlaying = true
//Checking whether the global variable is the same as the selected song url so that I don't have to fetch the asset again (fetching the asset plays the track from the beginning again)
if urlString == song.url {
player.play()
} else {
urlString = song.url
audio.fetchAsset()
audio.playAsset()
}
return
}
song?.isPlaying = false
player.pause()
print("Stop", (song?.title)!)
}
}
Do you have any idea why if the same track is saved twice in a row into the playlist and is selected to play - that the controls don't act like if it were a different track i.e. the desired functionality is that it would be treated as a completely different track where selecting it would play it from the beginning again whilst also retaining the ability to pause and play them individually.
If you need any more clarifications or extra code then please do not hesitate to ask. I really appreciate you taking the time to look over this problem.
EDIT
Here is the didSelectItemAt method as requested:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
currentItemIndex = indexPath.item
guard let cellToPlay = userPlaylistsCollectionView.cellForItem(at: IndexPath(item: currentItemIndex, section: 0)) as? UserPlaylistsCell else {
return
}
didSelectCell(for: cellToPlay)
}
EDIT
didDeselectMethod:
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cellToStop = userPlaylistsCollectionView.dataSource?.collectionView(userPlaylistsCollectionView, cellForItemAt: indexPath) as? UserPlaylistsCell {
if (cellToStop.song.isPlaying) {
didSelectCell(for: cellToStop)
print(cellToStop.song.isPlaying)
}
}
}
You're using
class
for your song objects. That means they're stored in memory by reference, so when you add the same song into the array twice, accessing either first or second instance will modify the same entity in memory. In your case I'd switch to usingstruct
for your "Data".struct
are value type and instances are copied instead of referenced. More on that in Swift docsAlso it's very important not to override system class names as
Data
is standardSwift
type alias that you will most likely use beyond the logic of your song models.The problem could be here
This is based on the fact that two songs have different url, but if the track are the same, they surely have the same url. Try different caching system, as example an Array that contains previous/currently played/ing song id (based on cell indexPath, for example).
Anyway, it's a bad behaviour to let user have the same track more than once in a playlist.
I don't think this is an issue with multiple referencing to the same item. It seems like your entire data in the array are not updated accordingly when a single data in the array is updated. Maybe I couldn't find the logic in your codes. Disregard if so.
Let say you have two tracks added to a playlist and one is played. Then when you play the the other track, the fist one's isPaying value should be updated to 'false' while the second one is turned on. You should make sure if that's happening.
Few recommendations.
or
Remove 'isPlaying' from Data class and let other manager or controller tracks the currently playing song so you don't need to worry about one or no song always playing.
Trying to pass the meaningful data to each view controller. For example, it seems like your Last view controller doesn't need to know about other playlist. So rather than getting a song from playlistArray[playlistIndex][indexPath.item], it's better if you have a data structure like playlist[indexPath.item].
Make sure your implementation is right when you do comparison between the object. Sounds like you can add two same songs to a same playlist and comparing song.url in play() function doesn't look promising. Even reference comparison sounds dangerous to me. What you should do? Hard to tell with given context. Again, it might be better if some other source let cell know if it's currently playing or not and the cell only displays its status.
Overall, your requirements don't look hard.