I have a table view full of songs, each cell has a Play button. Once you press the Play button the song plays and the Play button in the individual cell turns to a Stop button. Now this cell was done using View Tags, so when you scroll down further and as cells are reused, random cells that come into the view have a Stop button even though they were never selected. What is the best way to prevent this reuse from happening? Should I refactor my code into a custom UITableViewCell class and prevent reuse on the button? Or is there a quicker work around here?
You should remember playing state for song and update button status when reusing the cell (the same way you're updating other properties like title, artist etc).
For example, you can save playing song index in controller private variable (initially set it to -1) and compare it to indexPath.row when reusing the cell.
Related
I have a UITableView with cells that display an audio waveform image and a playback button. Tapping the button causes the audio to be played back, of course. In order to reduce memory usage, I have a single instance of AVAudioPlayer declared in my table view controller. I defined a protocol that has playAudio(url:URL) and stopPlayingAudio() methods and my table view controller conforms to this protocol. Anytime a new cell is dequeued, I assign the table view controller as the delegate for the cell so that when the user taps on the playback button in the cell UI, it calls the playAudio(url) delegate method. This seems to be working well enough but I've run into a problem now.
I'm calculating a percent complete value as the audio is playing and I'd like to update the table view cell UI with this value but I'm not sure how to reference the correct cell from the table view controller. It seems like the cell that was tapped on to start the audio playback could end up getting recycled if it scrolls off the screen (unless I'm misunderstanding how cells get dynamically dequeued). Is there a way to do this?
If you know what row of the table you're looking for, you can ask the table view for the corresponding cell:
guard let cell = tableView.cellForRow(at: indexPath) as? WaveformCell {
cell.fractionComplete = ...
}
There are a few ways in which you can achieve what you intend to achieve,
If you are maintaining a datasource to create a cell from(which you should if you are not), maintain the state of the cell, this can include the percentage played of the cell's url and the state whether the item isPlaying, which will be false by default.
Once the states are in place, you need to now update this state, so you will have to add create a protocol(say AudioStateObserverProtocol) for sending this data to the cell, this protocol may have a method which periodically updates the cell UI as the player plays (something like, updatePlayDuration: or something of this sort), this will make sure that you get the value of how much of the asset has played. So when the user taps the play button instead of calling playAudio(url:URL) you can update the protocol method to playAudio(url:URL, stateObserver: TheTableViewCell), which the table view controller will set to as the delegate of type AudioStateObserverProtocol.
protocol AudioStateObserverProtocol {
func updatePlayDuration(to time: CMTime)
}
Add another protocol method stopObserving(cell: TheTableViewCell) to the protocol you have defined with playAudio(url:URL) and stopPlayingAudio()
The next step is how to make sure that the cell on reuse does not still receive/use the update, to do this you can make sure that when you setup the cell in your cellForRow datasource methods you first call the stopObserving(cell: TheTableViewCell). In your implementation of this method inside the tableview controller check for the instance of the cell against the param of type AudioStateObserverProtocol and if same, set it to nil so that this cell does not get the updates again.
One important thing to keep in mind here is that, if your audio is still playing then you need to make sure that when the cell for that index is getting created it show updates, this is when you will check the isPlaying state of the datasource and if it is true set the cell as the observer of type AudioStateObserverProtocol
I've found some similar questions already on SO, but nothing that seems to address this specific problem.
I'm using a UITableView with around 25 dynamic cells. Each cells contains a hidden UIProgressView. When the user taps on the download button within the cell to download that item, the UIProgressView is displayed and it indicates the progress of the download.
I've achieved this by assigning a tag to each cell which is equivalent to its corresponding downloadItemID. Each instance of MyCell has its own progressBar property.
This works fine as long as the table view is not scrolled during the download. It works also when the user is downloading multiple items at the same time. The code looks like this:
UIProgressView *theProgressBar;
for (MyCell *cell in self.tableView.visibleCells)
{
if (cell.tag == downloadItemID) theProgressBar = cell.progressBar;
}
float progressPercentage = (float)completedResources / expectedResources;
[theProgressBar setProgress:progressPercentage animated:YES];
The problem is that when the user scrolls the table view, the cell and progress view are transferred to another cell. It's simple enough to reset and hide the progress view for the new cell, but when the original/downloading cell is scrolled back into view, no progress is visible.
I've tried caching active progress bars into a dictionary and reallocating them back to the original cell in cellForRowAtIndexPath, but this is giving me the same result: no visible progress after scrolling the cell off and on the screen. Even if I can get them to show up, I'm doubtful I can get this to work seamlessly by rolling my own caching method.
What I need is to keep cells in memory. But can I work around dequeueReusableCellWithIdentifier? This whole problem has arisen because I had to switch to a dynamic system of allocating the cells, but it is too dynamic. Can I create the cells dynamically, but keep them in memory all the time once created, or at least keep the ones that are currently downloading?
(I understand the reasons why cell reuse is the preferred way to go with table views.)
You are working against the framework. As vadian says in the comment, you need to separate the logic and state information from the cells and put them elsewhere.
Here is one way of doing it.
Create a class to hold each download state, like a download ongoing flag, download progress, title, URL, etc.
In your view controller, create and add all your download objects to an array when the view controller is created. The first object corresponds to the first row in the table.
Whenever you dequeue a cell, populate it with data from the array. The NSIndexPath row property serves as the index into the array.
All your updates (download progress) updates the download objects in the array, then update the cell content using the updated object. You can use UITableView cellForRowAtIndexPath to get the cell for a specific array index, if you get nil there is no need to update it.
I have a UITableView with a play icon in each cell. When you tap on that play icon it changes to a "Pause" icon. That works marvellously, but if you tap on an icon in another cell, how do I notify the previous cell to change back into a "play" icon? Basically I think I need a way to notify all the UIImage views inside cells at once. All of them for the whole table
You should have a var for playing indexPath.
When tap on another cell:
For visible cell: checking in array of visible cells with playing indexPath. -> if indexPath is in visible cell change 'pause' to 'play'
Then -> assign playing indexPath to current cell.
For unloaded cell: check it in cellForRowAtIndex. if indexPath != playingIndexPath -> 'play'
Sorry, I can't write ios code because you didn't show any. Hope this help
Three simple steps which you need to do in the sequence
1.In tableView:cellForRowAtIndexPath: datasource method, set the default image of the imageView as 'play'.
2.When the imageView of any cell is tapped, call the tableView reloadData method, which will reload the imageView of all cells to the default image.
3.Now change the imageView of the tapped cell to 'pause'.
As the second step sets the imageView of other cells to default, no separate notification is required. Happy coding :)
I am using a UICollectionView, in which one UICollectionViewCell covers the entire screen. Inside my UICollectionViewCell, I give people the opportunity to add text (UILabel), images (UIImageView) and color (UIColor). Now when the user navigates to the next cell i want the color, labels and image views to be displayed exactly as they were added on the previous cell. The users also have the option to pinch and pan the labels and images. In short, how can i pass on data from one cell to another?
I see several ways how to do this depending on your realization:
use global properties/ivars to store the selected data from user input.
In either case you probably handle UITextFiledDelegate methods in your controller or extract the cell by indexPath and copy values from the current cell to the next one.
And when the user presses the "Continue" button you:
1) If you create all you collectionViewCells at once in cellForItemAtIndexPath, then you should only reload the necessary cell via - (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths and set the values you have saved previously.
2) If you create cells but the next one is not ready (for instance you save the memory) - all almost the same - you add a new cell to the collectionView and read the data from your properties.
3) If you have not a "Continue" button or/and user can swipe the cell in every moment - so you can reload the cell in scrollViewDidScroll(or scrollViewWillBeginDragging) or extract the existent by indexPath and modify it without reloading.
In my TableView, the bottom cell has an "Add Cell" button that is used to..well, add cells. However, the app crashes if the user reorders the tableivew by moving a normal informational cell to the last position, making the "Add Cell": cell next to last.
I was able to prevent moving the "Add Cell" cell using tableView:canEditRowAtIndexPath:. I need something like tableView:canMoveRowAtIndexPath, but I only want to stop movement if they try to move it to the last position. Ideally, I'd need a hypothetical message like tableView:canMoveRowAtIndexPath:toIndexPath.
Any ideas?
I think you can do this by implementing tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: in your table view's delegate. Make the method return an index path that has a lower row number than your “Add Cell” row.