Iterating over UITableViewCell for AVSpeechSynthesizer - ios

I've seen everywhere that it's bad practice to iterate through a tableView but I'm afraid I have no other alternative. I want to have the AVSpeechSynthesizer read the content of the labels that are inside my custom UITableViewCells.
Of course, this could be done directly using the source array of the tableView but the problem is that I want to have the synthesiser read the labels and at the same time highlight the words that are being spoken.
I have done some tests but the problem is that as the cells are being reused by the table view, I end up with empty cells...
What is the right way of ensuring my cells are not empty ?
I've tried scrolling the tableView to ensure the cells aren't empty but it doesn't work either.
Here is my code so far ...
func scrollToRow(rowToGo: Int) {
self.collectionView.scrollToItem(at: self.indexPath as IndexPath, at: .centeredVertically, animated: true)
let colorCell = self.collectionView.dequeueReusableCell(withReuseIdentifier: "ColorCell", for:indexPath as IndexPath ) as! ColorCell
let myString = colorCell.colorLabel.text!
print(myString)
self.speakString(sender: self, str:(myString))
}
#IBAction func playPauseButton(_ sender: UIButton) {
if !self.speechSynthesizer.isSpeaking{
let btnImage = UIImage(named: "pausedButton.png")
self.playPauseButton.setImage(btnImage , for: UIControlState.normal)
self.isPaused = false
// var indexPath:Double = 0
self.indexPath = NSIndexPath(row: 0, section: 0)
for i in 0...36 {
print(i)
self.indexPath = NSIndexPath(row: i, section:0)
self.scrollToRow(rowToGo:i)
// self.indexPath = NSIndexPath(index:i)
// self.collectionView.selectItem(at: indexPath as IndexPath, animated: true, scrollPosition: .centeredVertically)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt
indexPath: IndexPath) -> UICollectionViewCell {
let myCell = collectionView.dequeueReusableCell(withReuseIdentifier:
"ColorCell", for: indexPath) as! ColorCell
myCell.colorLabel.text = self.wordList[indexPath.row]
myCell.colorImage.image = self.images[indexPath.row]
return myCell }
My collectionView is displayed correctly and all my labels inside the UICollectionViewCells contain text. However, when I press the play button, the first time, nothing gets spoken. If I scroll back and press again, only 3 or 4 cells actually exist and get their label.text printed and spoken while all the other indexes are empty.
Any suggestion would be welcome

Related

Auto Select Middle Visible Cell Of Collection View

I'm trying to select and highlight the middle cell of the visible cells in a collection view at any given time. The collection view in question displays days for six months forwards and back.
I've tried using the scroll view delegates and the collection view delegates. But all that works is select and highlight code in didSelectItem() collection view delegate.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("delegate called")
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
collectionView.cellForItem(at: indexPath)?.backgroundColor = UIColor.highlightCellGreen()
if let cell = collectionView.cellForItem(at: indexPath) as? ClientListDateCollectionViewCell{
monthLabel.text = cell.monthName
monthLabel.text = monthLabel.text?.capitalized
}
I tried to select the middle cell while scrolling using the viewDidScroll() delegate. But, I wasn't able to get the output I wanted.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleCellCount = dateCollectionView.indexPathsForVisibleItems.count
let cellCount = dateCollectionView.visibleCells.count
let visibleCells = dateCollectionView.indexPathsForVisibleItems[visibleCellCount-1/2]
if visibleCellCount>0{
let middle = visibleCellCount/2
let midValue = dateCollectionView.indexPathsForVisibleItems[middle]
dateCollectionView.selectItem(at: midValue, animated: true, scrollPosition: .centeredHorizontally)
}
How do I go about selecting the middle cell?
edit 1: The collection view starts on the leftmost point and then scrolls to the middle i.e, today's date
You can use delegate of UICollectionView (i.e: didHighlightItemAtIndexPath). just make sure to call collection view delegates on your desired time by calling reload function
self.collectionView.reloadData()
and in you collection view delegate just do this
func collectionView(collectionView: UICollectionView, didHighlightItemAtIndexPath indexPath: NSIndexPath){
var cell : UICollectionViewCell = UICollectionViewCell()
self.collectionView.cellForItemAtIndexPath = indexPath
//change highlighted color as of your need
cell.view.backgroundColor = UIColor.init(red: 25, green: 118, blue: 210).cgColor
}
This will highlight you selected item
Disable multiple selection (or selection entirely?) to make things easier.
collectionView.allowsMultipleSelection = false
On scrollViewDidScroll(_:) get the center point of the screen as CGpoint.
let center = collectionView.center
Use that information to get the index path of the center item
let indexPath = collectionView.indexPathForItem(at: center)
Select the item
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .top)
Suppose that you have the horizontal of displaying, and you want to have the auto scroll to the center of your item in datasource.
Creating a method and calling it immediately after your collection view is completely configured:
func scrollToCenterIndex() {
let centerIndex = LIST_OF_YOUR_DATA_SOURCE.count / 2
let indexPath = IndexPath(item: centerIndex, section: 0)
self.collectionView.scrollToItem(at: indexPath,
at: .right,
animated: false)
}
Inside the method:
public func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CELL,
for: indexPath) as? CustomCell else {
fatalError("Cannot create cell")
}
If indexPath.row == LIST_OF_YOUR_DATA_SOURCE.count / 2 {
// perform your hight light color to the cell
} else {
// reset your hight light color to default color
}
let model = LIST_OF_YOUR_DATA_SOURCE[indexPath.row]
cell.configure(model)
return cell
}
I think you can use a method to get the center point of collection view, and use this value to get the the middle of visible cell.
let centerPoint = self.view.convert(collectionView.center, to: collection)
Here is an example I did it with a tableView. You can apply it to your collection view with the same approach.
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var dataSource = Array(1...31)
var centerIndex: IndexPath?
func setCellSelected(cell: UITableViewCell, _ selected: Bool) {
cell.contentView.backgroundColor = selected ? .green : .white
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL")
cell?.textLabel?.text = String(dataSource[indexPath.row])
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = cell {
setCellSelected(cell: cell, indexPath.row == index.row)
}
return cell!
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// reset the previous hight light cell
if let centerIndex = centerIndex, let cell = tableView.cellForRow(at: centerIndex) {
setCellSelected(cell: cell, false)
}
// set hight light to a new center cell
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = tableView.cellForRow(at: index) {
setCellSelected(cell: cell, true)
centerIndex = index
}
}
}
I was also trying to do the auto-selection of the middle visible cell of the collection view, and I got the solution, here is the solution:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Reload Collection View
collectionView.reloadData()
// Find centre point of collection view
let visiblePoint = CGPoint(x: collectionView.center.x + collectionView.contentOffset.x, y: collectionView.center.y + collectionView.contentOffset.y)
// Find index path using centre point
guard let newIndexPath = collectionView.indexPathForItem(at: visiblePoint) else { return }
// Select the new centre item
collectionView.selectItem(at: newIndexPath, animated: true, scrollPosition: .centeredHorizontally) }
You need to use the Scroll view delegate function, scrollViewDidEndDecelerating. Reload the collection view first. Second, find the center visible point of the collection view. Third, using the center visible point, find the indexPath of collection view and finally use the index to select the item in the collection view.
I know I answered this question a little late, still thinking that it will be helpful for someone.
Cheers!

How can I access a button added in a collection view cell?

I added buttons in my collection view cells by the code below, where 'myButton' refers to the buttons I try to make access to.
When I click some button outside the collectionView, I want one of my buttons to have its background image changed to have the background of the button clicked, which I tried with 'sendToBox' function below;
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! ItemCollectionViewCell
cell.myButton.setTitle(self.items[indexPath.item], for: .normal)
cell.backgroundColor = UIColor.white
cell.layer.borderColor = UIColor.black.cgColor
cell.layer.borderWidth = 0.5
return cell
}
...
#IBAction func keyClick(_ sender: UIButton) {
...
sendToBox(object: sender)
...
}
...
func sendToBox(object sentObject: UIButton) {
let imageToSend = sentObject.backgroundImage(for: .normal)
let imageIdentity = sentObject.restorationIdentifier
let cell = self.collectionView(ItemCollectionSet, cellForItemAt: IndexPath(item: 0, section: 0))
cell.myButton ## blah blah not working!
}
So I want to make a direct access to one of the cells I created, and then change the button inside it to have a different appearance. I'm stuck in this matter for a whole day, please help me out.

Swift 4 : UITableView cellForRow crash

I can't understand what I am doing wrong. I have a UITableView called TblView_Categorie, what I am trying to do is get the cell at desired indexpath:
Here is my code:
let myRecordindex = IndexPath(row: myRecordcheck, section: 0)
let cell = tblViewcategorie.cellForRow(at: myRecordindex) as! categorieTVC
The problem is if MyRecordIndex is shown screen (no scrolling needed) everything working fine
If MyRecordIndex is not shown on screen (scrolling needed) I have this error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
If I manually scroll to the desired index and I enter the desired MyRecordIndex everything working fine
I tried to automatically scrolling using the code below: the scroll work but still same error
self.tblViewCategorie.scrollToRow(at: myRecordindex, at: UITableViewScrollPosition.middle, animated: true)
Why I have this error? All I am trying to do is getting the cell at desired indexpath to change color, font, etc.
// UPDATE cellForRowAt Implementation
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! categorieTVC
if let categorieName = self.fetchedResultsController.object(at: indexPath).categorieName{
cell.setListeDesCategories(categorieName: categorieName)
}
return cell
}
// Update code and crash screen shot
You can't explicit unwarp
let cell = TblView_Categorie.cellForRow(at: MyRecordIndex) as! CategorieTVC
as cell my be not visible so it will be nil -> crash
try
let cell = TblView_Categorie.cellForRow(at: MyRecordIndex) as? CategorieTVC
if(cell != nil)
{
}
Edit : scroll to the cell and dispatch after the cellForRow
let myRecordindex = IndexPath(row: myRecordcheck, section: 0)
self.tblViewCategorie.scrollToRow(at: myRecordindex, at: UITableViewScrollPosition.middle, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0 )) {
let cell = TblView_Categorie.cellForRow(at: MyRecordIndex) as? CategorieTVC
if(cell != nil)
{
}
}

How to update the button tag which is part of UICollectionViewCell after a cell is deleted in UICollectionView?

Here's a problem which I have been stuck at for quite some time now.
Here's the code
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({() -> Void in
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
self.collectionViewLove?.reloadData()}, completion: nil)}
I have a button inside each UICollectionViewCell which deletes it on clicking. The only way for me to retrieve the indexPath is through the button tag. I have initialized the button tag in
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
However every time I delete, the first time it deletes the corresponding cell whereas the next time it deletes the cell follwing the one I clicked. The reason is that my button tag is not getting updated when I call the function reloadData().
Ideally, when I call the reloadData() ,
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
should get called and update the button tag for each cell. But that is not happening. Solution anyone?
EDIT:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: UIImage(named: "preloader"))
let item = self.wishlist?.results[indexPath.row]
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.text = item?.title
cell.itemName.numberOfLines = 1
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.price.adjustsFontSizeToFitWidth = true
cell.deleteButton.tag = indexPath.row
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = indexPath.row
cell.buyButton.backgroundColor = UIColor.blackColor()
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
A couple of things:
You're doing too much work in cellForItemAtIndexPath--you really want that to be as speedy as possible. For example, you only need to register the nib once for the collectionView--viewDidLoad() is a good place for that. Also, you should set initial state of the cell in the cell's prepareForReuse() method, and then only use cellForItemAtIndexPath to update with the custom state from the item.
You shouldn't reload the data until the deletion is complete. Move reloadData into your completion block so the delete method is complete and the view has had time to update its indexes.
However, it would be better if you didn't have to call reloadData in the first place. Your implementation ties the button's tag to an indexPath, but these mutate at different times. What about tying the button's tag to, say, the wishlist item ID. Then you can look up the appropriate indexPath based on the ID.
Revised code would look something like this (untested and not syntax-checked):
// In LoveListCollectionViewCell
override func prepareForReuse() {
// You could also set these in the cell's initializer if they're not going to change
cell.layer.borderColor = UIColor.grayColor().CGColor
cell.layer.borderWidth = 1
cell.itemName.numberOfLines = 1
cell.price.adjustsFontSizeToFitWidth = true
cell.buyButton.backgroundColor = UIColor.blackColor()
}
// In your UICollectionView class
// Cache placeholder image since it doesn't change
private let placeholderImage = UIImage(named: "preloader")
override func viewDidLoad() {
super.viewDidLoad()
collectionView.registerNib(UINib(nibName: "LoveListCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "Cell")
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! LoveListCollectionViewCell
cell.imgView.hnk_setImageFromURL(NSURL(string: (wishlist?.results[indexPath.row].image)!)!, placeholder: placeholderImage)
let item = self.wishlist?.results[indexPath.row]
cell.itemName.text = item?.title
if(item?.price != nil){
cell.price.text = "\u{20B9} " + (item?.price.stringByReplacingOccurrencesOfString("Rs.", withString: ""))!
}
cell.deleteButton.tag = item?.id
cell.deleteButton.addTarget(self, action: "removeFromLoveList:", forControlEvents: .TouchUpInside)
cell.buyButton.tag = item?.id
cell.buyButton.addTarget(self, action: "buyAction:", forControlEvents: .TouchUpInside)
return cell
}
func removeFromLoveList(sender: AnyObject?) {
let id = sender.tag
let index = wishlist?.results.indexOf { $0.id == id }
let indexPath = NSIndexPath(forRow: index, inSection: 0)
collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
wishlist?.results.removeAtIndex(index)
}
It's probably not a good idea to be storing data in the cell unless it is needed to display the cell. Instead your could rely on the UICollectionView to give you the correct indexPath then use that for the deleting from your data source and updating the collectionview.
To do this use a delegate pattern with cells.
1.Define a protocol that your controller/datasource should conform to.
protocol DeleteButtonProtocol {
func deleteButtonTappedFromCell(cell: UICollectionViewCell) -> Void
}
2.Add a delegate property to your custom cell which would call back to the controller on the delete action. The important thing is to pass the cell in to that call as self.
class CustomCell: UICollectionViewCell {
var deleteButtonDelegate: DeleteButtonProtocol!
// Other cell configuration
func buttonTapped(sender: UIButton){
self.deleteButtonDelegate.deleteButtonTappedFromCell(self)
}
}
3.Then back in the controller implement the protocol function to handle the delete action. Here you could get the indexPath for the item from the collectionView which could be used to delete the data and remove the cell from the collectionView.
class CollectionViewController: UICollectionViewController, DeleteButtonProtocol {
// Other CollectionView Stuff
func deleteButtonTappedFromCell(cell: UICollectionViewCell) {
let deleteIndexPath = self.collectionView!.indexPathForCell(cell)!
self.wishList.removeAtIndex(deleteIndexPath.row)
self.collectionView?.performBatchUpdates({ () -> Void in
self.collectionView?.deleteItemsAtIndexPaths([deleteIndexPath])
}, completion: nil)
}
}
4.Make sure you set the delegate for the cell when configuring it so the delegate calls back to somewhere.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
//Other cell configuring here
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("identifier", forIndexPath: indexPath)
(cell as! CustomCell).deleteButtonDelegate = self
return cell
}
}
I was facing the similar issue and I found the answer by just reloading collection view in the completion block.
Just update your code like.
let indexPath = NSIndexPath(forRow: sender.tag, inSection: 0)
collectionViewLove?.performBatchUpdates({
self.collectionViewLove?.deleteItemsAtIndexPaths([indexPath])
self.wishlist?.results.removeAtIndex(indexPath.row)
}, completion: {
self.collectionViewLove?.reloadData()
})
which is mentioned in UICollectionView Performing Updates using performBatchUpdates by Nik

How to access cells that will appear, before they appear?

I have a collection view, but cell that are not appearing in the screen i cannot access, 1...5 ok, and those at index parth row 6, 7, and they have to be scrolled to bee seen
func Action0(sender: UIButton!) {
for i in 0...7 {
if i != 0{
var indexPath = NSIndexPath(forRow: i, inSection: 0)
var cell = collectionView!.cellForItemAtIndexPath(indexPath)
cell!.backgroundColor = UIColor.whiteColor()
}else{
var indexPath = NSIndexPath(forRow: 0, inSection: 0)
var cell = collectionView!.cellForItemAtIndexPath(indexPath)
cell!.backgroundColor = UIColor.blueColor()
}
}
}
How to acces those cells that are not yet visible?
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as! CollectionViewCell
cell.backgroundColor = UIColor.blackColor()
cell.textLabel.text = "\(indexPath.section):\(indexPath.row)"
cell.buttonView.setBackgroundImage(UIImage(named: "Good"), forState: UIControlState.Normal)
cell.buttonView.addTarget(self, action: Selector("Action\(indexPath.row):"), forControlEvents: UIControlEvents.TouchUpInside)
return cell
}
Cells that are not yet visible do not exist. Table views and collection views only create just the cells that are currently visible. When you scroll, the cells that go out of view get recycled and reused for the new index path(s) that come into view.
You need to think in terms of your data model, which holds the data for each index path, and cells, which display a view of of subset of some of that data.

Resources