I have created a User Onboarding as a Collection View with 5 cells (pages).
I have 2 types of Collection View Items: welcomeCell which is always a first and single cell in Onboarding (indexPath.item == 0) and tutorialCell (all other cells) which has a tableView and can scroll its content vertically.
I want to add the next behaviour:
user scrolls tableView content and swipes to the next page
if user swipes back to the page he scrolled I want the page layout to be reloaded like it was initially set.
For now if a user swipes to the page he scrolled, he will see its position where he ended the scroll.
I suggests that collectionView content can be reloaded in didEndDisplaying method:
func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item != 0 {
self.collectionView.reloadItems(at: [indexPath])
}
}
But with this code I receive a strange cells behaviour: I can swipe up tableView on one page and the came content position will be on another page, but some page can be loaded with normal content position. Here is a GIF: GIF
I also tried collectionView.reloadData() in scrollViewDidScroll and scrollViewDidEndDecelerating but receive the similar behaviour.
Would you be so kind to share some experience and help to understand what's the best practice for my case? Maybe I should find a way to not reload a page content at all, but reload imageView height anchor in constraints?
Try with DispatchQueue method:
if indexPath.item != 0 {
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
After some experiments and information research I ended up with following results:
I slightly modified my initial goal: with below code if user, while swiping, will decide to stay on the current page, then data won't be reloaded. In other cases - reload.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OnboardingCell", for: indexPath) as! OnboardingCollectionViewCell
// reset the current tableView position to initial one
if cell.tableView.window != nil {
cell.tableView.contentOffset = CGPoint.zero
cell.tableView.layoutIfNeeded()
cell.tableView.reloadData()
}
return cell
}
private var activePage = 0 {
didSet {
if activePage != oldValue {
// the method will fire cellForItemAt code
collectionView.reloadData()
}
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
for cell in collectionView.visibleCells {
activePage = collectionView.indexPath(for: cell)?.row ?? 0
}
}
Also if you have a navigation controls, like previous and next buttons, don't forget to reload data like so:
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
collectionView.reloadData()
}
Related
I have implemented expand/ collapse animation, for a UICollectionView with dynamic cell height (Because different cell has different content).
This is the summary of my implementation
I am using UICollectionViewCompositionalLayout, because I want the cell able to adjust its own height to accommodate its content. (https://stackoverflow.com/a/51231881/72437)
I am using UIStackView in the cell. Reason is that, once I hide one of the UITextViews in the cell, I do not want the hidden UITextView to still occupy the space. Using UIStackView can avoid me from dealing with zero height constraint.
I am using performBatchUpdates and layoutIfNeeded to achieve the animation, based on https://stackoverflow.com/a/69043389/72437
Here's the final outcome - https://www.youtube.com/watch?v=2uggmpk0tJc
As you can see, the overall effect isn't really smooth, espcially when I toggle in between "Color" and "Print PDF", which are having larger content height.
This is what happen when I tap on the cell
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
var indexPaths = [IndexPath]()
for i in (0..<isExpanded.count) {
if isExpanded[i] != false {
isExpanded[i] = false
indexPaths.append(IndexPath(item: i, section: 0))
}
}
if isExpanded[indexPath.item] != true {
isExpanded[indexPath.item] = true
indexPaths.append(IndexPath(item: indexPath.item, section: 0))
}
collectionView.performBatchUpdates({}) { _ in
collectionView.reloadItems(at: indexPaths)
collectionView.layoutIfNeeded()
}
}
Do you have any idea, what else thing I can try out, so that the animation will look smoother? This is the complete code example for demonstration
https://github.com/yccheok/shop-dialog/tree/1a2c06b40327f7a4d6f744f1c3a05a38aa513556
Thank you!
You can get close to your goal by changing didSelectItemAt to this:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
for i in (0..<isExpanded.count) {
if i == indexPath.item {
// toggle selected row
isExpanded[i].toggle()
} else {
// set all other rows to false
isExpanded[i] = false
}
if let c = collectionView.cellForItem(at: IndexPath(item: i, section: 0)) as? CollectionViewCell {
c._description.isHidden = !isExpanded[i]
}
}
collectionView.performBatchUpdates(nil, completion: nil)
}
The default animation / compression of elements when showing and hiding elements in stack views is not always acceptable though. If you want to try to refine it, take a look at this discussion:
Change default StackView animation
I'm trying to create a view showing the dates for all weekdays in one single row with horizontal scrolling and pagination enabled. Similar to how it works in the iPhone's calendar app, one full week should be visible at once.
To do that, I created a collectionView and set it up to display those seven items. When the user scrolls to the left (to see the next week), the function collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) is called. Here I add the next items and reload the collectionView.:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item > numberofitems - 10 {
numberofitems += 15
DispatchQueue.main.async {
self.myCollectionView.reloadData()
}
}
The collectionView is then reloaded:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! myCollectionViewCell
dayComponent.day = -3 * 7 - Calendar.current.component(.weekday, from: Date()) + indexPath.item + 1 + indecesadded
cell.setup(date: Calendar.current.date(byAdding: dayComponent, to: Date())!)
}
This seems to work pretty well.
However, what I also need, is being able to scroll in the other direction (backwards) and add new cells before the visible ones. I tried using the same method and adding the contentOffset:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.item < 10 {
numberofitems += 14
indecesadded += 14
DispatchQueue.main.async {
self.myCollectionView.reloadData()
self.myyCollectionView.setContentOffset(CGPoint(x: self.myCollectionView.contentOffset.x + 2 * self.view.bounds.width, y: 0), animated: false)
}
}
}
The problem is that after adding the new cells (mid-scrolling), the scrolling stops abruptly. When I manually beginn scrolling again, the paging seems to be off, meaning that the last day of one week does not correspond to the first day of the next - 1. (see images below).
before scrolling to previous week:
after scrolling:
after manually scrolling collectionView again:
So the question is: How do I insert items before the currently existing cells in a collectionView with paging enabled without messing up the scrolling and paging?
maybe you should try using scrollViewDidScroll like:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offset = scrollView.contentOffset
lastContentOffset = offset //save the offset if u need to set it later
if offset.x > 30{
print("right")
scrollRight()
} else if offset.x < -30 {
scrollLeft() // here you can implement the code bellow preformBatchUpdates
print("left")
}
}
if you want to have a sort of jumpy experience, like when u scroll it turns the page but it doesn't show how the numbers smoothly go away like on built in IOS Calendar on IPhone, that's the code i've tried for that:
collectionView.performBatchUpdates(
{
collectionView.insertSections([0])
sectionIndex += 1
}, completion: nil)
collectionView.performBatchUpdates({
collectionView.deleteSections([sectionIndex - 1])
sectionIndex -= 1
}, completion: nil)
that is implied when i override scrollViewDidScroll and scroll left for example. same is done for the other direction, the idea is that you keep maintaining only 1 section, so the adding of the sections doesn't mess up the position of the items in collectionView. i am actually working on the same project so if someone could provide a sample code for how i can be done like on the IPhone calendar it would also help me, hope that snippet gives you any new ideas or solves your problem
the answer here here works but i get some weird results
I need to make a request when the user saw the UITableViewCell, I used func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) but this method call for each UITableViewCell, before it's displayed, but I need a delegate when UITableViewCell appeared in the screen.
What you need to do is detect when scrolling occurs by implementing the scrollViewDidScroll(_:) delegate method. When the delegate method is called you can determine which cells are visible by checking the tableView.indexPathsForVisibleRows property. Note that this property returns the IndexPath for all cells that are both fully visible and partially visible.
Assuming you that you are only concerned with cells that are fully visible, you can check the frame for each row to determine if it is in fact fully visible.
To determine which cells are newly visible, you need to keep track of cells that were previously visible and compare the two sets.
This code will let you know as soon as soon as a cell is displayed:
var previousVisiblePaths = Set<IndexPath>()
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard let visiblePaths = tableView.indexPathsForVisibleRows?.filter ({
let rect = tableView.rectForRow(at: $0)
return tableView.bounds.contains(rect)
}).reduce(into: Set<IndexPath>(), { result, indexPath in
result.insert(indexPath)
}) else { return }
let newVisiblePaths = visiblePaths.subtracting(previousVisiblePaths)
if newVisiblePaths.count > 0 {
print("Just displayed: \(newVisiblePaths)")
}
previousVisiblePaths = visiblePaths
}
I have a vertical UICollectionView with flow layout and I'm trying to implement an infinite scroll behaviour. When the view comes near to the end, the api request 10 more registers on the server side and then I update the collection data. But when I do that my table don't keeps on the same cell I was before the reload. How can I keep the position after a reload?
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if indexPath.row == viewModel.numberOfItemsInSection - 3 {
self.viewModel.fetchNextRepositoriesPage() { shouldUpdateData in
self.repositoriesListView.repositoriesCollectionView.reloadData()
self.repositoriesListView.layoutIfNeeded()
}
}
}
private func reloadCollectionData() {
let offset = collectionView.contentOffset
collectionView.reloadData()
view.layoutIfNeeded()
collectionView.contentOffset = offset
}
EDIT:
I noticed that this problem only occurs when I use estimadedItemSize on my collectionView. When the itemSize is a constant the problem does not occur.
I want to make an infinite scroll collection view, so in this code I add 10 to the number of cells every time the index path is equals to the last cell item, and then reload the data in the collection. The function works; I can scroll infinitely, but if I stop, and then scroll a little up or down, everything is blank. The cells aren't showing.
My theory is that it has something to do with dequeueReusableCellWithReuseIdentifier, and it decides to only show the cells that are currently on the screen.
View when scrolling (I added the numbers to the cells outside of Xcode)
View with the missing cells above when scrolling a bit after stopping
private let reuseIdentifier = "Cell"
private var numberOfCells = 20
class CollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath)
if indexPath.row == numberOfCells - 1 {
numberOfCells += 10
self.collectionView?.reloadData()
}
cell.contentView.backgroundColor = UIColor.blueColor()
return cell
}
override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return numberOfCells
}
}
Calling reloadData from cellForItemAtIndexPath is bad. Use other delegate method for that, for example, scrollViewDidScroll.
var numberOfCells: Int = 20 {
didSet {
if numberOfCells != oldValue {
self.collectionView?.reloadData()
}
}
}
I am doing similar things in Table View and it is working for me. The only difference is that if indexPath.row is last cell then I am calling another function which do some stuff (required for me) and calling reloadData() into that function only. Try this way, I am not sure, but it may solve your problem.
Swift 4.2
reload it like this.
DispatchQueue.main.async(execute: collectionView.reloadData)