UITableView cellForRow(at:) crashes randomly while scrolling - ios

I have a requirement to show border around user's avatar image but only for the user cell at center of table. Also, the border width is proportional to its distance from center.
So I've my cell update logic in UIScrollView delegate method scrollViewDidScroll API and then get the middle cell using UITableView contentOffset.y. Once I know the center point of table I ask the table to give me cell at that point and then updating its value.
However, the call to tableView.cellForRow(at:) randomly throws an exception and crashes. I do guard for nil value but don't know how to handle this crash due to exception.
Also, note centerLevelCellIndexPath value looks good to me when that exception is raised - section 0 row 3 ( My UITableView always have 1 section and about 5-6 rows)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView is UITableView {
let tableViewCenterContentOffset = CGPoint(x: tableView.center.x, y: tableView.contentOffset.y + tableView.bounds.height/2)
guard let centerLevelCellIndexPath = tableView.indexPathForRow(at: tableViewCenterContentOffset) else {
return
}
guard let cell = tableView.cellForRow(at: centerLevelCellIndexPath), // <--- Crashes here
let centerLevelCell = cell as? MyCustomCell,
let collectionView = centerLevelCell.collectionView else {
return
}
Here is the screenshot of exception call stack -

Related

Access collectionView from scrollViewDidEndDecelerating

I have a UICollectionView called dayPicker that scrolls horizontally, and lets you select the day of the month. When the user stops scrolling (scrollViewDidEndDecelerating), I want the app to do something with that day, accessible from the cell's label. All of the answers I have seen online are similar to this: https://stackoverflow.com/a/33178797/9036092
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var someCell : UICollectionViewCell = collectionView.visibleCells()[0];
// Other code follows...
}
When I try to access collectionView from inside the scrollViewDidEndDecelerating function, I get a Ambiguous use of collectionView error. When I substitute the actual name of my UICollectionView (dayPicker), it errors out with "Unexpectedly found nil while implicitly unwrapping an Optional value".
My question is: how do you get to collectionView from inside the scrollViewDidSomething function? Currently my scrollViewDidEndDecelerating function is inside a UICollectionViewDelegate in my view controller, and I have also tried putting it in a UIScrollViewDelegate extension.
Current code:
extension PageOneViewController: UICollectionViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let centerPoint = CGPoint(x: UIScreen.main.bounds.midX, y: scrollView.frame.midY)
print(centerPoint) // Successfully prints the same center point every time scrolling stops
let indexPath = collectionView.indexPathForItem(at: centerPoint) // Ambiguous error
let indexPath = dayPicker.indexPathForItem(at: centerPoint) // Fatal error
}
}
Screenshot of scrollable UICollectionView in question:
I also have another method of when the user taps on the day, and it is working flawlessly. Trying to complete the experience with the scrolling ending.
Xcode 11.4.1/Swift 5
I figured it out, for those who come across this thread looking for the same answer. Big thanks to this answer here for a different issue: https://stackoverflow.com/a/45385718/9036092
The missing ingredient was to cast scrollView as a UICollectionView so that you can access the collectionView's cell properties.
Working code:
extension PageOneViewController: UICollectionViewDelegate {
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let collectionView = scrollView as! UICollectionView // This is crucial
let centerPoint = CGPoint(x: UIScreen.main.bounds.midX, y: scrollView.frame.midY)
let indexPath = collectionView.indexPathForItem(at: centerPoint)
let centerCell = collectionView.cellForItem(at: indexPath!) as! MyCustomCell
let selectedDay = centerCell.dayLabel.text //
print(selectedDay) // Prints the value of the day in the center of the collectionView, as a string
}
}

Change cell constraint dynamicly in tableView

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.

Making a textView in a collectionView cell the firstResponder while navigating cells

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

Reordering cell interactively with resized imageView

I want to be able to reorder cells in my collectionview but have the cell that is moving's image be scaled down to a smaller size for the duration of the reorder. I am able to get the cell to resize while moving but there is an issue when the cell crosses over another cell and attempts to reorder. At the moment the cell reaches another cell, it temporarily resizes back to normal size, causing a flickering when moving it around.
I have followed this blog description for using Apple's methods to interactively reorder cells in a UICollectionView. The methods I am using are listed under the "Reordering Items Interactively" section of this page.
I have a UICollectionView subclass, a UICollectionViewCell subclass, and extensions for UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, and UICollectionViewDelegate.
The way I am resizing the cell is by adding a boolean to my cell's subclass that is set to false by default but true when beginning and updating cell movement, and back to false when ending or canceling. In my sizeForItem method for the UICollectionViewDelegateFlowLayout extension, I am checking the boolean and returning a size accordingly.
It has been difficult to debug where the methods are getting the size to display when the cell's image returns to normal size for a split second, causing the flickering behavior, as each method is called many times to update the movement smoothly.
I am not married to resizing the cell in this way, if it is the cause of the issue, I'm just not sure how to resize it and prevent it from flickering when crossing over other cells in the collectionview.
Thank you in advance for any suggestions.
UPDATE
Some code samples.
I update the size of the bounds of imageView in beginInteractiveMovementForItem to shrink it initially. By the time the collectionview calls the update method, the size gets reset, but the cell gets its size from sizeForItem, below is my logic for that.
As a side note, I have also tried resizing the bounds of the cell itself in beginInteractiveMovement in addition to the imageView's bounds, but it had no effect so I removed that.
Here's how I'm handling resizing the imageView:
In the collection view's parent:
#objc func handleLongGesture(_ sender: UILongPressGestureRecognizer) {
switch(sender.state) {
case .began:
let location = sender.location(in: self.collectionView)
guard let selectedIndexPath = self.collectionView.indexPathForItem(at: location) else {
return
}
guard let dragCell = collectionView.cellForItem(at: selectedIndexPath) as? ActivityPhotoGalleryCell else { return }
UIView.animate(withDuration: 0.17, animations: {
dragCell.center = location
})
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(sender.location(in: sender.view))
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
In the collection view subclass:
override func beginInteractiveMovementForItem(at indexPath: IndexPath) -> Bool {
guard let dragCell = cellForItem(at: indexPath) as? CustomCellObject else { return false }
draggingCell = dragCell
dragCell.isDragging = true
return super.beginInteractiveMovementForItem(at: indexPath)
}
And here is my size for item logic:
var sizeForItem = CGSize(width: widthPerItem, height: widthPerItem)
let cell = collectionView.cellForItem(at: indexPath) as? CustomCellObject
if (cell?.isDragging ?? false) {
sizeForItem.height *= 0.7
sizeForItem.width *= 0.7
}
return sizeForItem

Place UITableViewCell beneath siblings

I'm trying to build a custom UITableView that acts in a similar way to the reminders app.
Instead of the top-most visible cell scrolling off the display, I want it to be covered by the next cell so the cells appear to stack on top of one another as you scroll.
Currently I am using:
override func scrollViewDidScroll(scrollView: UIScrollView) {
let topIndexPath: NSIndexPath = tableView.indexPathsForVisibleRows()?.first as! NSIndexPath
let topCell = tableView.cellForRowAtIndexPath(topIndexPath)
let frame = topCell!.frame
topCell!.frame = CGRectMake(frame.origin.x, scrollView.contentOffset.y, frame.size.width, frame.size.height)
}
But the top cell is always above the second cell - causing the second cell to scroll under it.
Also if I scroll quickly this seems to misplace all of my cells.
EDIT: Have fixed this. Answer posted below for future reference.
For anybody searching this in the future.
Simply loop through all the visible cells and set their z position to their row number (so each cell stacks above the previous one).
The if statement tells the top cell to remain at the contentOffset of the scrollview and for all other cells to assume their expected position. This stops the other cells being offset if you scroll too quickly.
override func scrollViewDidScroll(scrollView: UIScrollView) {
// Grab visible index paths
let indexPaths: Array = tableView.indexPathsForVisibleRows()!
var i = 0
for path in indexPaths {
let cell = tableView.cellForRowAtIndexPath(path as! NSIndexPath)
// set zPosition to row value (for stacking)
cell?.layer.zPosition = CGFloat(path.row)
// Check if top cell (first in indexPaths)
if (i == 0) {
let frame = cell!.frame
// keep top cell at the contentOffset of the scrollview (top of screen)
cell!.frame = CGRectMake(frame.origin.x,
scrollView.contentOffset.y,
frame.size.width,
frame.size.height)
} else {
// set cell's frame to expected value
cell!.frame = tableView.rectForRowAtIndexPath(path as! NSIndexPath)
}
i++
}
}

Resources