How to detect a UIScrollView has finished scrolling when animation is false? - ios

I have a collectionView called timeline that is scrolling programmatically on the second to last line here:
internal func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if !onceOnly {
let indexToScrollTo = IndexPath(row: self.posts.count - 1, section: 0)
collectionView.scrollToItem(at: indexToScrollTo, at: .left, animated: false)
let firstPost = posts.first?.timeStamp
let firstOfFirstMonth = firstPost?.startOfMonth()
let diff = posts.last?.timeStamp.months(from: firstOfFirstMonth!)
//self.currentPostMonth = diff
let monthCellIndexPath = IndexPath(row: diff!, section: 0)
timeline.scrollToItem(at: monthCellIndexPath, at: .centeredHorizontally, animated: false)
onceOnly = true
}
}
later.. I am attempting to detect that this has finished scrolling with
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView == timeline {
print("Did finish")
}
}
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
if scrollView == timeline {
print("Did finish")
}
}
Neither print statement fires once the scroll has completed. I think partially because animation = false. When I set that to true, it prints Did finish correctly - I think specifically scrollViewDidEndScrollingAnimation prints even though scrollViewDidEndDecelerating still does nothing because its being scrolled programatically.
How can I detect that this scroll has finished?

I think because you compare the collectionView with the scrollView
if scrollView == timeline
which is false , BTW you can use
func scrollViewDidScroll(_ scrollView: UIScrollView)
If there is no animation then in the next line after
timeline.scrollToItem(at: monthCellIndexPath, at: .centeredHorizontally, animated: false)
run what you want as it's will occur serially where scrolling ended successfully , no need to set it inside scrollView's delegate methods whether it's called or not

Related

How to achieve smooth expand/ collapse animation in UICollectionView with dynamic cell height?

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

iOS swift - UICollectionView scrollToItem at scrollPosition of the item

How is it possible to scroll to item and set the horizontal collectionView UICollectionView.ScrollPosition to the exact position of the item (not the .right, .left etc) and at the same time enable the scroll to the user? I tried this:
var selectedLineIdexPath: IndexPath? {
didSet {
if let index = self.selectedLineIdexPath {
self.linesCollectionView.scrollToItem(at:index, at: .centeredHorizontally, animated: false)
}
}
}
Where the selectedIndexPath is set on the:
final func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath)
depending on the value of my data.
The result is that the collectionView scroll to this item but when I try to select fetch and select another one its automatically scroll to the center.

collectionView didSlectRow bug

Hello on a specific viewController of my project i have a UICollectionView with a custom class cell. But i have a big problem this that func :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("tapped on a cell")
}
when is click on a cell and instant realease (normal click) it does nothing, just nothing.
if i press and hold about 1s without release the finger it become grey, highlighted
And if i press and hold at least 3 seconds release the finger didSelectItemAt is executed correctly.
I tried to do the same on another project and that's work great but not on this VC and i really don't find the problem. The VC Bugged is of addTest clas in Main.storyboard
The insight of Mojtaba Hosseini is very clever, but the answer given might not be quite correct.
It turns out that there is a UITapGestureRecognizer on the main view; if it recognizes before the tap on the cell, it prevents cell selection. But if you merely set cancelsTouchesInView to false on that gesture recognizer, then they both operate, and that seems unlikely to be what is wanted. We surely want the cell tap and not the tap gesture recognizer tap.
The correct solution is thus to give the tap gesture recognizer a delegate and implement gestureRecognizerShouldBegin. Here, we look to see where the tap is. If it is within the bounds of a cell, we return false; otherwise we return true. We thus mediate between the cell tap and gesture recognizer tap.
Here is a possible implementation, demonstrated in a highly simplified form:
extension UIView {
func isDescendant(of whattype:UIView.Type) -> Bool {
var sup : UIView? = self.superview
while sup != nil {
if (whattype == type(of:sup!)) {
return true
}
sup = sup!.superview
}
return false
}
}
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UIGestureRecognizerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
let t = UITapGestureRecognizer(target: self, action: #selector(tap))
self.view.addGestureRecognizer(t)
t.delegate = self
}
#objc func tap(_:UIGestureRecognizer) {
print("tap")
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("select")
}
func gestureRecognizerShouldBegin(_ gr: UIGestureRecognizer) -> Bool {
if let v = gr.view {
let loc = gr.location(in: v)
if let v2 = v.hitTest(loc, with: nil) {
return !v2.isDescendant(of: UICollectionViewCell.self)
}
}
return true
}
}
As you can see, we look to see whether the tap is inside a collection view cell; if it is, our gesture recognizer is prevented from recognizing, and the selection succeeds immediately.
Probably there is a UIGesture or another interactable thing underneath the collection view. You should DISABLE its ability to cancel touches in view in interface builder:
or in code:
myTapGestureRecognizer.cancelsTouchesInView = false

UITableView Scrolls to top after insert row

I have an expandable UITableView. When sections tapped, they expand or collapse with animation (Scroll). My problem is that there is a weird animation when expanding or collapsing headers. UITableView scrolls to top and then goes to the tapped cell. In addition, when there is no expanded cell, sometimes, It doesn't scroll to top and there is a big space between top header and top view of UITableView.
My problem is that I need to scroll to expanded section and also get rid of scroll to top bug.
I tried below solution but didn't work for me:
prevent table view to scrolling top after insertRows
It also looks like same problem with below question, but can't figure out how to implement it.
Why does my UITableView "jump" when inserting or removing a row?
How I toggle selection:
func toggleSection(header: DistrictTableViewHeader, section: Int) {
print("Trying to expand and close section...")
// Close the section first by deleting the rows
var indexPaths = [IndexPath]()
for row in self.cities[section].districts.indices {
print(0, row)
let indexPath = IndexPath(row: row, section: section)
indexPaths.append(indexPath)
}
let isExpanded = self.cities[section].isExpanded
if(isExpanded){
AnalyticsManager.instance.logPageEvent(screenName: analyticsName!, category: "Button", action: Actions.interaction, label: "\(self.cities[section].name) Collapse Click")
}else{
AnalyticsManager.instance.logPageEvent(screenName: analyticsName!, category: "Button", action: Actions.interaction, label: "\(self.cities[section].name) Expand Click")
}
self.cities[section].isExpanded = !isExpanded
// This call opens CATransaction context
CATransaction.begin()
// This call begins tableView updates (not really needed if you only make one insertion call, or one deletion call, but in this example we do both)
tableView.beginUpdates()
if isExpanded {
tableView.deleteRows(at: indexPaths, with: .automatic)
} else {
tableView.insertRows(at: indexPaths, with: .automatic)
}
// completionBlock will be called after rows insertion/deletion animation is done
CATransaction.setCompletionBlock({
// This call will scroll tableView to the top of the 'section' ('section' should have value of the folded/unfolded section's index)
if !isExpanded{
self.tableView.scrollToRow(at: IndexPath(row: NSNotFound, section: section) /* you can pass NSNotFound to scroll to the top of the section even if that section has 0 rows */, at: .top, animated: true)
}
})
if self.scrollToTop(){
self.tableView.setContentOffset(.zero, animated: true)
}
// End table view updates
tableView.endUpdates()
// Close CATransaction context
CATransaction.commit()
}
private func scrollToTop() -> Bool{
for sec in self.cities{
if(sec.isExpanded){
return false
}
}
return true
}
I'm giving height of cell inside;
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
How I declare headers;
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = DistrictTableViewHeader()
header.isColapsed = !self.cities[section].isExpanded
header.customInit(title: self.cities[section].name, section: section, delegate: self)
return header
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 60
}
EDIT: Solution in this question (Setting estimated height to 0) looks like working when inserting row. However, I still have bug when deleting rows. Bottom header goes to center of tableview and then goes to bottom after collapse header.
iOS 11 Floating TableView Header
You can try using below code. Just get the last content offset of your tableview. Then do the update and reassign the content offset.
let lastOffset = tableView.contentOffset
self.tableView.beginUpdates()
self.tableView.layer.removeAllAnimations()
self.tableView.endUpdates()
tableView.contentOffset = lastOffset
Instead of tableView.beginUpdates() and tableView.endUpdates(), In my Code i'm using tableView.reloadData() for expanding and contracting the particular section, You can call reloadData when you need to provide expansion of section.This results that you don't have the problem of animation scroll to the top. And works fine in my project where I have to show number of rows in particular section on a click of button which includes in that section.
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
return 1
}
// Ignore the If Else statements it's just when do you need expansion of section.
else {
if showMore == true {
return self.userPoints.rewardsData[section - 1].count - 1
}
else {
return 0
}
}
}
Also Don't Forget to increase or decrease the number of rows to that particular section accordingly.Previous line is important to avoid any crash.
Simple Solution swift3 and Above
use below extension as
eg: tableViewOutlet.tableViewScrollToBottom(animated: true)
extension UITableView {
func tableViewScrollToBottom(animated: Bool) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
let numberOfSections = self.numberOfSections
let numberOfRows = self.numberOfRows(inSection: numberOfSections-1)
if numberOfRows > 0 {
let indexPath = IndexPath(row: 0, section: 0)
self.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.top, animated: animated)
}
}
}
}
I also facing same issue but after read some tutorials and research & Analysis I got the this issue occurred due to height of cell when you expand the section at that tableview count height of cell from 0 to 120(as per your cell height).
In my case I solved that issue using estimated height of cell.
I hope that will help you,
Thanks

How can I change the current position of UIPageControl according to the CollectionViewCell's item

my View hierarchy is something like:
TableView-->tableViewCell-->CollectionView-->CollectionViewCell-->imageView
and in my tableViewCell I have some other items (textView, labels and a UIPageControl) so now am trying to change the currentPage of PageControl according to the item of CollectionViewCell ( same as carousel ) but I don't know why UIPageControl is not changing its position this is what I tried:
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
// here scrollView is my collectionView
if scrollView != tableView {
pagerControll.currentPage = Int(scrollView.contentOffset.x / self.view.frame.size.width)
}
}
I also tried this (for remembering the current position of Item in a cell after scrolling through the tableView)
override func tableView(tableView: UITableView,
willDisplayCell cell: UITableViewCell,
forRowAtIndexPath indexPath: NSIndexPath) {
guard let cell = cell as? NotesTableViewCell else { return }
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
cell.collectionViewOffset = storedOffsets[indexPath.row] ?? 0
cell.pageControll.currentPage = calculateCurrentPage(storedOffsets[indexPath.row] ?? 0)
}
above am fetching the contentOffset of each Row from an array of (contentoffset) so that I can show the previous positions Of CollectionViewCell's items, when tableView reuses the cell its working fine for the items of my collectionView cell but not for my UIPageControl
func calculateCurrentPage(offSet : CGFloat) -> Int{
if offSet >= self.view.frame.size.width && offSet < (self.view.frame.size.width*2) {
return 1
}else if offSet >= (self.view.frame.size.width*2) {
return 2
}else {
return 0
}
}
What's wrong here? or how to do it?
UPDate Extra Codes:
//inside my tableViewCell
func setCollectionViewDataSourceDelegate
>
(dataSourceDelegate: D, forRow row: Int) {
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.tag = row
collectionView.reloadData()
}
//in TableView
extension NotesTableViewController: UICollectionViewDelegate, UICollectionViewDataSource , UICollectionViewDelegateFlowLayout {
func collectionView(collectionView: UICollectionView,
numberOfItemsInSection section: Int) -> Int {
pagerControll.numberOfPages = attachedImgUrlDict[collectionView.tag]!.count
return attachedImgUrlDict[collectionView.tag]!.count
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
let newSize = CGSizeMake(collectionView.frame.size.width , collectionView.frame.size.height)
return newSize
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 0, 0, 0)
//t,l,b,r
}
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
if scrollView != tableView {
if scrollView.contentOffset.x >= 320 && scrollView.contentOffset.x < 640{
pagerControll.currentPage = 1
}else if scrollView.contentOffset.x >= 600 {
pagerControll.currentPage = 2
}
//pagerControll.currentPage = Int(scrollView.contentOffset.x / self.view.frame.size.width)
print(scrollView.contentOffset)
}
}
}
Check #1
You'll receive callbacks for scroll events for UICollectionView as soon as you set a UICollectionViewDelegate on the collectionView instance.
Seems like you might be missing setting up UICollectionViewDelegate in following call
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
Can you verify you're receiving callbacks for UICollectionView scroll events?
Check #2
Say you are receiving callbacks properly now, Can you check your logic works properly with page index? A good thing would be to add a print statement that would show you the page index that you are calculating.
let pageIndex = Int(scrollView.contentOffset.x / self.view.frame.size.width)
print("pageIndex == \(pageIndex)")
Check #3
Say you are calculating it right, Can you check if cell.pageControl is populated with properly with UIPageControl instance you need to update.
Maybe you need to check your IBOutlet connections?
Check #4
Inside prepareForReuse() callback, you need to make sure that pageControl is set to some initial value like 0.
Adding a small delay while updating the pageIndex could work if you see inconsistencies like it is updating sometimes and sometimes it's not.
Hope it helps.

Resources