Sync animation for scroll in collection view within table view - ios

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)
}
}

Related

Table view batch updates while scrolling

I've built a pagination mechanism for my table view so when the user scrolls to the last row in section the app loads additional content if possible. When additional content gets loaded, I perform a batch update on my table view like this:
guard
let sectionIndex = self.sections.firstIndex(of: .itemsSection),
!loadedItems.isEmpty
else { break }
let previousNumberOfRows = self.tableView.numberOfRows(inSection: sectionIndex)
let additionalIndexPaths = self.generateIndexPaths(
startIndex: previousNumberOfRows,
count: loadedItems.count,
section: sectionIndex
)
self.tableView.performBatchUpdates({
self.items.append(contentsOf: loadedItems)
self.tableView.insertRows(at: additionalIndexPaths, with: .automatic)
}, completion: nil)
and here's a function that generates index paths to insert new rows at:
private func generateIndexPaths(startIndex: Int, count: Int, section: Int) -> [IndexPath] {
var indexPaths: [IndexPath] = []
for row in startIndex..<startIndex + count {
indexPaths.append(IndexPath(row: row, section: section))
}
return indexPaths
}
It works perfectly fine until the update happens while table view is scrolling. Additional content is getting loaded so fast that the scroll animation have no time to finish. It starts to jump and additional cells pop with a broken animation. Since my table view uses UITableView.automaticDimension, I thought it was because of the wrong estimated height for newly created cells, so I implemented tableView(_:estimatedHeightForRowAt:) -> CGFloat method and gave those cells a pretty accurate height value. But it didn't help fixing the jumps. After trying all possible UITableView.RowAnimation fitting my need, I decided to completely disable the update animation. Here's how I'm currently updating the table view:
self.items.append(contentsOf: loadedItems)
UIView.setAnimationsEnabled(false)
self.tableView.insertRows(at: additionalIndexPaths, with: .none)
UIView.setAnimationsEnabled(true)
This approach gets rid of the update animation and at the same time introduces another problem: it feels like the table view is unresponsive during the update animation, like if I set isUserInteractionEnabled = false for a second. So what is the best way to update the table view with new rows with no animation and jumps? Thanks in advance :)

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

Scroll to last element of TableView take too much time

I'm trying to simulate a Whatsapp Chat any cell will have an image (for tail of the bubble), a bubble which is just View with color and some corner radius and a label which will represent the text of the message.
I've put a print before and after the call
self.messagesTableView.reloadData()
Once the after print is called tableView keeps some time doint I don't know what till the data is shown. And same happens with Insert row at indexpath, it takes some time till show the insert animation.
func displayMessages(viewModel: GroupChatMessages.GetChatMessages.ViewModel) {
let displayedMessage = viewModel.displayedMessages
print ("i'm here!")
messages = displayedMessage!
//self.messagesTableView.performSelectorOnMainThread(Selector("reloadData"), withObject: nil, waitUntilDone: true)
self.messagesTableView.reloadData()
print ("i'm here2!")
firstTime = false
self.setVisible(hiddenTableView: false, hiddenChatLoader: true)
self.scrollToLastMessage(false)
self.messagesLoaded = true
}
I've tried to do dispatched with queue, and the commented line before reloadData(), but nothings works and nothing represent a significative time.
Maybe could be for the image of the bubble? I don't know. I have this image saved on Assets, so I'm not downloading it from internet.
self.setVisible just hide the loader and show the tableView but I've tried too moving it up and nothings changes. Any further information you need let me know. Thanks!
EDIT:
Well I've seen that the problem comes from the scroll to last cell, this is where it takes the major part of the time.
func scrollToLastMessage(animated: Bool) {
let section = 0
let lastItemIndex = self.messagesTableView.numberOfRowsInSection(section) - 1
let indexPath:NSIndexPath = NSIndexPath.init(forItem: lastItemIndex, inSection: section)
self.messagesTableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: animated)
self.scrollDownButton.hidden = true
}
There is a posibility to optimize that scroll, because I have to do a Scroll because once the data is loaded, the first I've see is the top row of the tableView, but I would like to see the bottom one (last). Thanks!
methods like reloadData() should be considered as UI methods and it's mandatory to call them in main thread:
DispatchQueue.main.async { tableView.reloadData() }
It's better not to use reloadData() function unless a significant amount of cells need to refresh or data source has been changed instead use this method to add new rows:
tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation)
and for refreshing cell:
tableView.reloadRows(at: [IndexPath], with: UITableViewRowAnimation)
also if the cell has a considerable amount of images and rendering, use this code to make scrolling faster:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
// ADD THESE TWO LINE
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
}
Using these ways will boost loading speed significantly
Finally the solution that I've found to avoid dying while waiting scrolling to last element any single time, is swapping orientation of table
tableView.transform = CGAffineTransformMakeRotation(-M_PI);
cell.transform = CGAffineTransformMakeRotation(M_PI);
Now headerView and footerView are reversed. For exemple, if you would like insert rows at (visually) at the bottom of the TableView with this configuration you should add it at position 0 forRow: 0 atSection: "WhereYouAre". This way when you add new element, no scroll is needed, because scroll is automatically. Amazing and strange answer IMHO.
I've found this solution here:
Solution Link
#Christos Hadjikyriacou solved there.

UITableView doesn't scroll automatically if the datasource changes

I have a UITableView with two arrays as datasource. I also have one UISegmentedControl to switch between these arrays. I use the segmentedControlChanged method to call the reloadData method.
This works fine.
But there is one problem.
For example arrayOne has 1 item and arrayTwo has 10 items.
So if I switch to the arrayTwo and scroll to the bottom of the tableview and after that switch back to arrayOne the tableview stays scrolled down and i don't see any item. But if I start scrolling, the tableview "jumps" automatically to the first item.
So my question is, how can i trigger this behaviour automatically when the tabledata source changes?
You can use any one of following two lines after reloading table
let indexPath = NSIndexPath(forRow: 0, inSection: 0)
self.tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Top, animated: true)
or
mainTableView.setContentOffset(CGPoint.zero, animated: true)
Firstly i don't know how you change data source of tableview but i use this method for my data source switches.
I create 3 arrays;
var mainArr = [String]()
var sortedArr = [String]()
var arrToReturn : [String] {
if segmentedController.selectedIndex == 0 {
return mainArr
}
else {
return sortedArr
}
}
I always return arrToReturn for tableView data source.
When it comes to your scrolling issue, it sounds like you are reloading tableView before changing its dataSource.
After that when you start scrolling, tableView reloads cells and can't find old items and automatically sets its scrolling position to start of screen.
Also do you have any code about setting tableviews scrolling position even if it is irrelevant? If you have you should check it.

Custom UIViewController transition where UITableViewCell grows to full screen and back?

I haven't been able to find any examples of this online. How can I achieve the following effect? Instead of the standard slide-to-left effect when tapping a table row, I'd like the view controller transition animation to look like the following:
User taps a cell in the table
The cell starts growing to fill the screen, pushing other rows above and below it "offscreen".
As the cell grows, cells elements (text, images, etc.) cross-fade into the new view's contents until the new view completely fills the screen.
I'd like to also be able to interactively transition back into the table view by dragging up from the bottom edge, such that the reverse of the above is achieved. i.e. view starts shrinking back into a normal table view cell as the "offscreen" cells animate back into position.
I've thought about taking a snapshot of the table and splitting it up at the points above and below the cell, and animating these snapshots offscreen as part of a custom view controller transition. Is there a better way? Ideally I'd like to not take snapshots, as I may want to have animations, etc., still happening in the table view rows as they fade offscreen.
First thing first, i have seen your post today and his is written in swift programming language.
the follwing code is to expand the selected cell to the full screen, where the subviews will fade out and background image will expands.
First complete cellForRowAtIndexPath, then in didSelectRowAtIndexPath,
func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!) {
isSelected = true
selectedCellIndex = indexPath.row
tableView.beginUpdates()
var cellSelected: UITableViewCell = tableView.cellForRowAtIndexPath(indexPath)!
cellSelected.frame = tableCities.rectForRowAtIndexPath(indexPath)
println("cell frame: \(cellSelected) and center: \(cellSelected.center)")
let newCell = cellSelected.frame.origin.y - tableOffset
println("new cell origin: \(newCell)")
var tempFrame: CGRect = cellSelected.frame
variableHeight = cellSelected.frame.origin.y - tableOffset
tempFrame.size.height = self.view.frame.height
println("contentoffset: \(tableView.contentOffset)")
let offset = tableView.contentOffset.y
println("cell tag: \(cellSelected.contentView.tag)")
let viewCell: UIView? = cellSelected.contentView.viewWithTag(cellSelected.contentView.tag)
println("label: \(viewCell)")
viewCell!.alpha = 1
UIView.animateWithDuration(5.0, delay: 0.1, options: UIViewAnimationOptions.BeginFromCurrentState, animations: {
tableView.setContentOffset(CGPointMake(0, offset + self.variableHeight), animated: false)
tableView.contentInset = UIEdgeInsetsMake(0, 0, self.variableHeight, 0)
tableView.endUpdates()
cellSelected.frame = tempFrame
viewCell!.alpha = 0
println("contentoffset: \(tableView.contentOffset)")
}, completion: nil)
}
then update your heightForRowAtIndexPath, as
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if isSelected && (selectedCellIndex == indexPath.row) {
return self.view.frame.height
}else {
return 100
}
}
Ignore this if already solved. and this might be helpful to others.

Resources