How to find indexpath in collectionView - ios

I am developing player application. I have playlist and on collectionview there is a border which shows which song currently playing. When the song ends it automatically going on next song.
I want automatically update that cell as currentPlaying cell and reset previous cell as normal state.
For that reason I want find indexPath for current and previous played song.
func playingCompositionForReload(composition: Composition) {
composition.isPlaying = true
// find index path and reload one cell only
}
How could I find it?

You'll need to keep the current cell's indexPath in a var:
var currentlyActiveCellIndexPath: IndexPath
When the user selects a cell, set currentlyActiveCellIndexPath to that cell's indexPath (though this depends on how you start everything off - you may not need this if you start with the first cell automatically for instance, then you'd just set currentlyActiveCellIndexPath to IndexPath(item: 0, section: 0) )
func collectionView(_ collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: IndexPath) {
self.currentlyActiveCellIndexPath = indexPath
// etc, etc
}
You'll need to implement some sort of delegate pattern (where the cell calls a method on its delegate) to let your controlling code know that the song has finished, & when it gets the call, just call the function you've created on the next cell, which you find by incrementing the indexPath's item. Eg -
// delegate method called by cell
func songDidFinish() {
self.currentlyActiveCellIndexPath.item += 1
// Obviously you'll have to use whatever class you've created your cells as here, with the relevant methods, etc.
if let nextCell = self.collectionView.cellForItem(at: self.currentlyActiveCellIndexPath) as? SongPlayingCell {
nextCell.startPlaying()
}
}
Presumably, when a cell stops playing, it can set its own state.
If you're not sure about how to use delegates, that's another question, & there's plenty of info out there about that.

Related

How to move focus on specific cells in collection view?

I have a collection view scrollable in 2-dimensions. Each row is a section with some cells.
Now in each row only one item is special (will have enum state = special, otherwise normal).
I want a behavior such that when we move up or down and we were on a special cell we should jump to special cell of next row.
View that behavior here
I did it like this, I will be storing previous focused cell's indexpath in a variable and when I move to next cell then in collectionView(:canFocusItemAt:) I will check if previous was special and we are changing the section then only next cell is focusable if it's special.
func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -> Bool {
if let previousFocusIndex = focusIndex,
previousFocusIndex.section != indexPath.section,
let previousCell = collectionView.cellForItem(at: previousFocusIndex) as? EpisodeCell,
let nextCell = collectionView.cellForItem(at: indexPath) as? EpisodeCell,
previousCell.timeState == .special {
print("🟢#Searching_Episode: \(nextCell.description)")
return nextCell.timeState == .special
}
return true
}
But now I came to know about behavior of focus engine that its search area is based on current cell's width.
And is a cell in next row does not lies in that search area it will not focus there.
Problem I am facing
I tried googling but didn't find anything better and can't think what else I can try.
If you have an idea it will be really helpful. Thanks.
There are 2 parts to solve this problem.
Identify the user is seeing the special content shown in collection view[Special Item]
This can be achieved by checking the Visible Items and cross check with data source that if it is special
var indexPathsForVisibleItems: [IndexPath] { get }
Scrolling to Special Item in a particular section
Based on the out come of step 1 we should decide calling scrollToItem
func scrollToItem(at: IndexPath, at: UICollectionView.ScrollPosition, animated: Bool)
You may put this logic when you identify that user is scrolling the collection view by confirming to UIScrollViewDelegate, which is inherited by UICollectionViewDelegate.
protocol UIScrollViewDelegate //Protocol to use
func scrollViewDidScroll(UIScrollView)
//Tells the delegate when the user scrolls the content view within the scroll view.
We need to have optimised logic here such that scrollToItem is only called when there is a new row is showed.

Number of Lines for UILabel not Updating as Expected When TableViewCell is Tapped

I am changing the numberOfLines attribute on a label that lives in a custom UITableViewCell when the cell is tapped. However, this is not reflected in the UI until the second tap. The cell is configured as a prototype cell in the table view to initially have 2 lines.
Interestingly enough, when I print out the numberOfLines value before and after my tapped() function runs, the values start off different, and then synchronize - after the first tap, I see 2 lines before the function runs, then 0 lines after the function runs. However, after subsequent taps, I see the same value before and after my function, which makes it seem like it's not doing anything, even though the UI does stretch and shrink the cell, and the numberOfLines value is changed for the next time the didSelectRowAtIndexPath function runs.
I'm only seeing this behavior with tableView.reloadRows(). If I do a full update with tableView.reloadData(), the cell appropriately grows and collapses the first time it is tapped. However, this feels a bit ham-fisted and doesn't animate nicely like reloadRows() does.
TableView Implementation
func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? ReviewTableViewCell
else { return }
let data = tableData[indexPath.row]
print("old number of lines: \(cell.detailLabel.numberOfLines)")
//data.isOpen is set to false initially
cell.tapped(data.isOpen)
tableData[indexPath.row].isOpen = !data.isOpen
tableView.reloadRows(at: [indexPath], with: .fade)
print("old number of lines: \(cell.detailLabel.numberOfLines)")
// tableView.reloadData()
}
Custom Table View Cell method
func tapped(_ isOpen: Bool) {
if !isOpen {
detailLabel.numberOfLines = 0 }
else {
detailLabel.numberOfLines = 2 }
}
I am expecting this code to expand the cell once it is reloaded with tableView.reloadRows() if the numberOfLines is set to 0 and collapse the cell when it is set to 2. This does work, but only after tapping the cell two+ times. This should work with the first tap as well.
Here is a link of a gif that shows the issue: https://imgur.com/a/qe2uAXj
Here is a sample project that is similar to what's going on in my app: https://github.com/imattice/CellLabelExample
Just to be clear, to get this trick work UILabel generally must be constrained on each side to it's superview, in this way when it changes its intrinsicContentSize is able to push each side to accomodate the text.
Saying that, try to wrap the tapped method with those two methods:
tableView.beginUpdates()
if !isOpen {
detailLabel.numberOfLines = 0
}
else {
detailLabel.numberOfLines = 2
}
tableView.endUpdates()
Of course tableview must be set to automatic size:
tableView.estimatedRowHeight = <#What you want#>
tableView.rowHeight = UITableView.automaticDimension
I was able to work out what was going on. The problem is in two parts.
The first part is calling reloadRows(). This method is swapping out the cells with a new cell rather than updating the cell that already exists. Therefore, I'm changing the number of lines on that hidden swap cell rather than the cell that is in view. This behavior is mentioned in the docs:
Reloading a row causes the table view to ask its data source for a new cell for that row. The table animates that new cell in as it animates the old row out.
Additionally, I'm using structs as the data model for tracking the open status of the cell. In Swift, structs are copy-on-write, which means that if a value is changed on that struct, a new struct is created rather than changing the value of that struct I'm pointing to. This means the line tableData[indexPath.row].isOpen = !data.isOpen doesn't do anything useful - we look at the tableData struct at the index path, get it's isOpen value, copy a new struct and change that new struct's isOpen value, and then throw it out because the new struct is not assigned anywhere.
The solution is to not use the reloadRows() method and to either use
A) a class for the data object
B) replace the data at indexPath.row to the copied struct
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let cell = tableView.cellForRow(at: indexPath) as? CustomCell else { return }
var data = tableData[indexPath.row]
tableView.beginUpdates()
cell.tapped(isOpen: data.isOpen)
data.isOpen = !data.isOpen
tableData[indexPath.row] = data
tableView.endUpdates()
}

How to get indexPath of new center UICollectionViewCell after one has been deleted?

I have a horizontal collectionView called postsCollectionView where each is as wide as the screen. Currently I am computing the indexPath of the currentPost with:
let currentPostIndex = Int(self.postsCollectionView.contentOffset.x / self.postsCollectionView.frame.size.width)
let currentPostIndexPath = IndexPath(row: currentPostIndex, section: 0)
When a post gets deleted and I have reset the "currentPost", I have this hacky method in my View Controller:
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if (postsFetchedResultsController.fetchedObjects?.count)! > 0 {
print(postsCollectionView.indexPathsForVisibleItems,postsCollectionView.contentOffset.x) //prints [],375.0
//HACK
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { // change 2 to desired number of seconds
let currentPostIndex = Int(self.postsCollectionView.contentOffset.x / self.postsCollectionView.frame.size.width)
let currentPostIndexPath = IndexPath(row: currentPostIndex, section: 0)
self.currentPost = self.postsFetchedResultsController.object(at: currentPostIndexPath)
}
} else {
print("no items left")
}
}
Basically there is an NSFetchedResultsController that calls the collectionView's deleteItem method when the data source changes. I am using the callback didEndDisplaying cell in an attempt to - when the cell has been totally deleted, set self.currentPost to the now displayed post that has taken its place.
The problem is that this callback is called I think when the cell is deleted, but the animation hasn't finished so its still offset at 375 (the width of the screen) so it computes the current index path at 1 even though it should be 0 because now there is only 1 post in the collectionView.
Is there a way to wait until the animation is finished as well? Or is there a better way to compute the new indexPath of the collection view cell in the center after one has been deleted?
1) Why don't you set currentPost to nil and recalculate it when you actually need it, instead of as soon as you have deleted the cell
2) You can use UICollectionView.indexPathForItem(at point: CGPoint) to get your IndexPath. It is probably a little safer. I guess the point you pass in would be
CGPoint(x:self.postsCollectionView.contentOffset.x, y:0)
The y position would be dependent on how you place you cell in the collection view.
If you need the result immediately. You know the current index before deletion. If the cell being deleted has index less than or equal to that then increment your current index, if the deleted cell's index is greater don't change it.

swift textview and labels return empty text in UITableviewcell

Current Situation:
I have a UITableView with custom cells. In the cells are 1 label and 1 textview.
Scrolling is enabled for the UITableView.
Below the Table, I have a button to save the entries from the textview.
My Problem is:
I get only the value from the first row.
From the other rows, I get always an empty text, but only if the user has scrolled.
I don't understand why and I hope you can help me.
Here is my Code:
#objc func save()
{
for i in 0..<labels.count
{
let indexPath = IndexPath(item: i, serction: 0)
let cell = self.SearchTable.dequeueReusableCell(withIdentifier: "searchCell",
for: indexPath) as! SearchCell
print("Index: \(indexPath)")
if(cell.EditField.text = ""
{
continue;
}else{
...
}
}
}
Debugger
First Row: FirstRow
Other Rows: SecondRow
Cells in a tableview are reused, so a cell at an index path that is not visible could have values from a completely different cell. If you want to get all the cells that are currently visible on the screen, you could use tableView.visibleCells docs to get all the cell that are currently on screen and extract data from there.
Alternatively, you could choose to not implement cell reuse and make your table view static. You can do this in Interface Builder, or you could also choose to create all the cells up front and return your pre-made cells in tableView(_:cellForRowAt:). Note that a setup like this is okay for small datasets, but has terrible performance for larger sets so be aware that this might not be the best way to do things. It really depends on your situation. This method of doing things would end up looking a bit like this:
var cells = [UITableViewCell]()
override func viewDidLoad() {
// do all kinds of stuff
for field in fields { // or whatever else mechanism you use as your datasource
let cell = UITableViewCell()
// configure your cell
cells.append(cell)
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return cells[indexPath.row]
}
The third and last way you might want to solve this is to add a delegate to your cells and set the view controller as the delegate. If the text changes, call the delegate with the cell's index path and the new text value. You can then store this text somewhere in the view controller and read it from there when you save rather than pulling it from the cell's textfield. Personally I would prefer this method.
Rather than iterate over the cells in the tableview, you could just get the data from the data source.
Whenever the text in the cell's text field changes you could update the data source and then use the information from there to perform your save.
You must be having some kind of data source anyway, otherwise what happens to the text when the cell scrolls off the screen and comes back on again? If you aren't storing the text somewhere then you've got nothing to populate the cell with in the table views cellForRow(... delegate method.

Displaying two different cells in a collection view - Swift 2.0 iOS

I'm working on a "trading" application where I would like to have a static number of cells.
On load, users will see 5 cells, each displaying a label that says "Add."
When a "player" is added, that cell displays the players information, the other 4 cells still display the "Add" label. Another is added, 2 cells have player information, 3 have the "Add"
I'm having a hell of a time with this. Can anyone point my in the right direction? I have custom labels setup, I think my logic may just be off on how to perform this correctly.
You need to subclass the UICollectionViewDelegate and UICollectionViewDataSource protocols in your viewController, then you need to implement the numberOfItemsInSection and cellForItemAtIndexPath functions.
Additionally to that you need to create two type of cells in your storyboard and subclass them, in the following code i will suppose that you call AddedPlayerCell and DefaultCell your cells, i will suppose that each cell has a label called labelText too.
let players = ["Player1","Player2"] //players added till now
let numberOfCells = 5
//Here you set the number of cell in your collectionView
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return max(players.count,numberOfCells);
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
if((indexPath.row + 1) < self.players.count){ //If index of cell is less than the number of players then display the player
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("yourIdentifierForAddedPlayerCell", forIndexPath: indexPath) as! AddedPlayerCell
cell.labelText.text = self.players[indexPath.row] //Display player
return cell;
}else{//Else display DefaultCell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("yourIdentifierForDefaultCell", forIndexPath: indexPath) as! DefaultCell
cell.labelText.text = "Add"
return cell;
}
}
In order to manage two different cell types, you can:
Create 2 prototype cells for your collection view. Give one the identifier "Add" and the other "Info". The "Add" cell prototype will contain the label "Add", and the "Info" cell prototype will contain fields to display the player info.
Add an array property to your class which keeps track of which cells are displaying "Add".
var showingAdd = [true, true, true, true, true]
In cellForItemAtIndexPath, check the showingAdd array to determine which identifier to use when dequeueing the cell:
let identifier = showingAdd[indexPath.row] ? "Add" : "Info"
let cell = dequeueReusableCellWithIdentifer(identifier...)
if !showingAdd[indexPath.row] {
// configure the cell with the proper player info
// retrieve info from info property array item created in
// step 4.
let player = playerInfo[indexPath.row]
cell.playerName = player.name
...
}
When a cell is selected in didSelectItemAtIndexPath, check if it is showing add and then process it accordingly:
if showingAdd[indexPath.row] {
// query user to get player info
// store the info in a property array indexed by `indexPath.row`
playerInfo[indexPath.row] = PlayerInfo(name: name, ...)
showingAdd[indexPath.row] = false
// trigger a reload for this item
collectionView.reloadItemsAtIndexPaths([indexPath])
}

Resources