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.
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 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
I have a tableview (style - grouped) which contains more rows than can fit into its frame so you have to scroll to see the last row. When I delete the last row the tableview scrolls its content down so the new last row is at the bottom of the frame (kind of fill the empty space).
How can I prevent a UITableView from scrolling after I delete the last row in it? I want it to keep the empty space and do not scroll automatically.
I have tried to set content insets but it doesn't seem to prevent scrolling unfortunately.
EDIT: My code for removing the row:
func removeLastRow() {
let indexPath = IndexPath.init(row: self.messages.count - 1, section: 0)
self.messages.removeLast()
CATransaction.begin()
self.tableView.beginUpdates()
CATransaction.setCompletionBlock { () -> Void in
// do stuff, add new rows
}
self.tableView.deleteRows(at: [indexPath], with: .left)
self.tableView.endUpdates()
CATransaction.commit()
}
Retreive the cells size before deleting by calling tableView:heightForRowAt: and after deleting, set the vertical content offset of your table view manually to the current vertical content offset, plus the height of the cell.
let deletedCellHeight = tableView.heightForRowAt(indexPath)
Then:
tableView.contentOffset.x += deletedCellHeight
I'm facing a challenge when using UICollectionView and a related Layout. I'm trying to create a vertical UICollectionView with two columns, where as the cells in the left and right columns are positioned as displayed in the picture:
I do manage without problems to create the two columns but struggle finding the correct settings in my Layout in order to have the right column be offset by half the height of one cell. Note, all cells have the same calculated dimension based on the width of the screen (minus the spacing)...
Every cell in my data array has got a position index, I can thus easily find out whether a cell is positioned right or left based base on it (odd / even)
Any help is gladly appreciated
Here's how I would implement a subclass of UICollectionViewFlowLayout to achieve what you want.
class OffsetFlowLayout: UICollectionViewFlowLayout {
var verticalOffset: CGFloat = 0
override func prepare() {
super.prepare()
verticalOffset = 100 // Calculate offset here
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let attributes = super.layoutAttributesForItem(at: indexPath) else { return nil }
guard remainder(Double(indexPath.row), 2) != 0 else { return attributes }
// For each item in the right column, offset the y value of it's origin
attributes.frame.origin.y += verticalOffset
return attributes
}
}
As you can see, on our implementation of layoutAttributesForItem the first thing we do is to call super and store the value it returns. Then we check the index and if we are on the left column, we return what super gave us, which would be the "default" value.
If we're on the right column, we modify the attributes object to move the frame of the cell by the offset we want, and then return that one.
NOTE: I haven't tested this. There's a chance the UICollectionViewFlowLayout uses the previous cells to layout the next cells, in which case you would only need to modify the first element in the right column, just change guard remainder(Double(indexPath.row), 2) != 0 else { return attributes } to guard indexPath.row == 1 else { return attributes }
The thing I want to achieve is a gallery, it can contain up to 300 images, using a UIScrollView will result in non optimized ram/cpu usage since it doesn't do pooling (correct me if I'm wrong), and it has a limitation on width, I can't make it 300 times the width of the screen (again correct me if I'm wrong).
so the right choice would be to use a UITableView
1 - how to make it scroll horizontally like in this gif ?
2 - how to make it snap to cells centers ?
EDIT
I'm using this, it gives the result I want but it has a problem, it has no tolerance for how much I should swipe to move to the next cell, I still need help..
func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool)
{
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell]
{
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0)
if (cellDelta < closestCellDelta)
{
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
func scrollViewWillBeginDecelerating(scrollView: UIScrollView)
{
// Find collectionview cell nearest to the center of collectionView
// Arbitrarily start with the last cell (as a default)
var closestCell : UICollectionViewCell = collectionView.visibleCells()[0];
for cell in collectionView!.visibleCells() as [UICollectionViewCell]
{
let closestCellDelta = abs(closestCell.center.x - collectionView.bounds.size.width/2.0)
let cellDelta = abs(cell.center.x - collectionView.bounds.size.width/2.0)
if (cellDelta < closestCellDelta)
{
closestCell = cell
}
}
let indexPath = collectionView.indexPathForCell(closestCell)
collectionView.scrollToItemAtIndexPath(indexPath!, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: true)
}
EDIT :-
I managed to do it without above code.
You should use UICollectionView with one cell or UIPageViewController to achieve this kind of effect. There is no way to use tableview horizontally. Somehow you can say UICollectionView as a horizontal representation of UITableView. UIPageViewcontroller is also good solution. You should read about both in detail and then decide what's more suitable for you!
and yes that's not good to use single scroll view with that much different size. It can create memory or performance issue!!
Update :
Have you tried to enable paging in your collection view's scroll view ?
look at screenshot below
Check the checkbox paging enable!
then after If you not feel smooth effect then you should try to uncheck adjust scroll view insets under viewcontroller under attribute inspector which you got by selecting your view controller
check the screenshot for that
EDIT by person who asked the question : -
adding this worked for me, as UICollectionView don't have
adjust scroll view insets option.
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
you can use UICollectionView and configure collection view cell
set collectionViewLayout size... This size based on above view
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return CGSizeMake(250, 150);
}