I have a collectionView and allow a user to dynamically add cells to the collectionView. The view controller only shows one cell at a time and I want the first textView (which is part of the cell) to become the firstResponder (and keep the keyboard visible at all times), which works fine when loading the view controller (as well as in one of the cases below).
I have created a method to detect the current cell, which I call every time in any of these cases: (1) user scrolls from one cell to another (method placed in scrollViewWillEndDragging), (2) user taps UIButtons to navigate from one cell to another, (3) user taps UIButton to create and append a new cell at the end of the array (which is used by the collectionView).
This is the method:
func setNewFirstResponder() {
let currentIndex = IndexPath(item: currentCardIndex, section: 0)
if let newCell = collectionView.cellForItem(at: currentIndex) as? AddCardCell {
newCell.questionTextView.becomeFirstResponder()
}
}
Now my problem is that this only works in case (1). Apparently I have no cell of type AddCardCell in cases (2) and (3). When I print the currentCardIndex, I get the same result, in all of the cases, which is very confusing.
Any hints why I wouldn't be able to get the cell yet in cases 2 and 3, but I am in case 1?
As a reference here are some of the methods that I am using:
//Update index and labels based on user swiping through card cells
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//Update index based on targetContentOffset
let x = targetContentOffset.pointee.x
currentCardIndex = Int(x / view.frame.width)
setNewFirstResponder()
}
And the other method, from which it doesn't work (case 3):
//Method to add a new cell at end of collectionView
#objc func handleAddCell() {
//Inserting a new index path into tableView
let newIndexPath = IndexPath(item: autoSaveCards.count - 1, section: 0)
collectionView.insertItems(at: [newIndexPath])
collectionView.scrollToItem(at: newIndexPath, at: .left, animated: true)
currentCardIndex = autoSaveCards.count - 1
setNewFirstResponder()
}
Regarding case 2,3 i think that the cell is not yet loaded so if let fails , you can try to dispatch that after some time like this , also general note if the cell is not visible then it's nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
setNewFirstResponder()
}
Also it can work if you set animated:false to scrollToItem
Related
I have custom layout with fullscreen cells. When removing cell from the left (it's not visible at the time), UICollectionView jumps to the next cell.
It's like current cell was at index 4 and when cell on the left removed the next cell has index 4 now and immediately scroll to the next cell.
Describing in 3 steps (A is cell that need to be fullscreen, x will be removed, o other cells, large letter is fullscreen):
ooooAoo
oooxAoo
oooaOo
But must keep this oooAoo
Here is my solution if it can help to anybody, did not found how to achieve desired offset natively, so just scrolling contentOffset to the desired position right after reloadData():
var currentCell: MyCollectionViewCell? {
return (visibleCells.sorted { $0.frame.width > $1.frame.width }.first) as? MyCollectionViewCell
}
//-----------------------------------------
//some model manipulating code, removing desired items here...
let currentList = currentCell?.parentList
reloadData()
if let list = currentList, let index = self.lists.firstIndex(of: list) {
self.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false)
}
currentCell is computed property, returns optional middle cell.
Detect which cell is the largest, because of custom flowlayout logic.
parentList is the model item, I can compare cell by it to make life
easier. I check which list was attached to the cell before
reloadData().
I have a large square grid (225 cells) constructed as a 2d UICollectionView and I would like blind users to be able to navigate the grid easily. Currently they would need to scroll horizontally through every square to go down. Ideally, I'd like to have a custom VoiceOver rotor that allows for left, right, up and down swiping to navigate. Is this possible?
If not, I would settle for having a custom rotor which moves up and down via left/right swipe. I've attempted to create one by getting the current indexPath and using the scrollToItem method to change it. The rotor can be selected but has no effect currently so I've done it wrong so far.
private func setupVerticalRotor() -> UIAccessibilityCustomRotor {
let vertRotorOption = UIAccessibilityCustomRotor.init(name: "Move vertically") { (predicate) -> UIAccessibilityCustomRotorItemResult? in
let forward = predicate.searchDirection == UIAccessibilityCustomRotor.Direction.next
let visibleCells = self.collectionView.visibleCells
if let firstCell = visibleCells.first {
if let indexPath = self.collectionView.indexPath(for: firstCell as UICollectionViewCell) {
if forward {
let newIndexPath = IndexPath(row: indexPath.row, section: indexPath.section + 1)
self.collectionView.scrollToItem(at: newIndexPath, at: UICollectionView.ScrollPosition.centeredVertically, animated: false)
}else {
let newIndexPath = IndexPath(row: indexPath.row, section: indexPath.section - 1)
self.collectionView.scrollToItem(at: newIndexPath, at: UICollectionView.ScrollPosition.centeredVertically, animated: false)
}
}
}
return UIAccessibilityCustomRotorItemResult.init(targetElement: self.collectionView , targetRange: nil)
}
return vertRotorOption
}
Ideally, I'd like to have a custom VoiceOver rotor that allows for left, right, up and down swiping to navigate. Is this possible?
The native rotor has already the item to navigate vertically.
For your code, I suggest to take a look at this custom rotor example + check your collection view cells update with breakpoints in Xcode.
I'm trying to update some constraints on a cell depending on the scroll progress inside my TableView. The goal is to recreate this effect : https://youtu.be/VMyNHq3CO04?t=416 (at 6:56)
I'm currently having all my cell with spaces and rounded corner. For now, I get the position of the offset of the scrollView of my tableView and run the following function :
func updateCellState(position: CGFloat) {
let numberOfItems = tableView.numberOfRows(inSection: 0)
for i in 0..<numberOfItems {
guard let cell = tableView(tableView, cellForRowAt: IndexPath(row: i, section: 0)) as? FMReceiptReviewCellWithContextualActions else {
break
}
tableView.beginUpdates()
self.stretch = position / 1000 > 1 ? 1 : position / 1000
cell.delegate?.updateCellState(position: position)
cell.setNeedsLayout()
cell.layoutIfNeeded()
tableView.endUpdates()
}
}
The updateCellState take the position of the scrollView to update my constraint proportionally.
But I've some major issues with this code :
In term of performance, the function is called way to often because of the scrollView (even when calling the updateCellState only by 30,40,... offset point)
My cells are not always updated and they need to be dequeue to be reload and display my cells with new parameters (a reloadData() of the tableView work, but not suitable for a smooth flow)
Do you have any idea how I could replicate the effect I mentioned above.
I have collection views (in plural) inside a table view of many sections. Just so we're clear, a single table view with many sections with only one row each being that row an individual collection view.
All set up is working just fine, the data is well divided and delegates are all wired up recognizing everything they need to recognize. My problem is kind of simple but difficult at the same time: I want to scroll to specific collection view's position whenever I need to find a specific cell in animated fashion.
So far I'm able to jump with no problem to both table section (indexPath.section) and collection item (indexPath.row). The issue arises when I need to scroll (simultaneously) with animation.
My findings so far
I'm only able to achieve my current goal deactivating scroll animations for UITableView (UICollectionView can perform well with/out it)
Whenever I set UITableView selectRow or scrollToRow animation flags to true then the app crashes (99% sure this happens because I'm trying to access and "invisible" section due to the animation hasn't shown it yet).
Relevant snippets of code
#IBOutlet weak var albumTableView: UITableView!
#IBOutlet weak var stickersCollectionView: UICollectionView!
func locateCell() {
...
let stickerIndex = methodThatReturnsExactIndex()
let sectionIndex = IndexPath(row: 0, section: stickerIndex.section)
albumTableView.selectRow(at: sectionIndex, animated: false, scrollPosition: .top)
let rowIndex = IndexPath(item: stickerIndex.row, section: 0)
stickersCollectionView.scrollToItem(at: rowIndex, at: 0, animated: true)
}
I was thinking in experiment with the UIScrollViewDelegate (detecting when the tableview and the collectionview stopped in order to perform the scrolling) but that would imply spreading global variables around the code and experience tough me that's just racing conditions waiting to happen. Any help will be appreciated.
First Scroll your tableView to that specific index with/without animation. This will make that cell visible now get your cell by providing that indexPath so you could access the collectionView object inside your tableViewCell. Then ask you collectionView to scroll to specific indexPath with/without animation.
Take another global bool to store that tableView is begin scrolling. Also store both indexPath used for collection and tableView and use tab
tableViewIsScrolling = true
let yourSelectedIndexPathForTableView = IndexPath(row: 0, section: 4)//store it globally
let yourSelectedIndexPathForCollectionView = IndexPath(row: 10, section: 0)//store it globally
tableView.scrollToRow(at: yourSelectedIndexPathForTableView, at: .middle, animated: false)
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if tableViewIsScrolling {
tableViewIsScrolling = false
//Perform your scrolling of CollectionView
guard let yourCell = tableView.cellForRow(at: yourSelectedIndexPathForTableView) as? YourCell else {return}
yourCell.collectionView.scrollToItem(at: yourSelectedIndexPathForCollectionView, at: .centeredHorizontally, animated: true)
}
}
using this code to scroll to bottom of collection view. however it scrolls to the second last one only.
private func scrollToBottom() {
let lastSectionIndex = (ChatCollectionView?.numberOfSections())! - 1
let lastItemIndex = (ChatCollectionView?.numberOfItemsInSection(lastSectionIndex))!-1
let indexPath = NSIndexPath(forItem: lastItemIndex, inSection: lastSectionIndex)
ChatCollectionView!.scrollToItemAtIndexPath(indexPath, atScrollPosition: UICollectionViewScrollPosition.Bottom, animated: false)
}
is anyone familiar with a bug like that
Your collection view's frame was under the visible area. You should set its bottom inside visible area.