When a user swipes, I run some code in scrollViewWillEndDragging and I need the targetContentOffset.pointee.y to do it:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let yAxis = targetContentOffset.pointee.y
let visibleItem = Int(yAxis / collectionView.frame.height)
// do something with visibleItem based on cell's current vertical scroll position...
}
The issue is I also do some autoscrolling, where the user doesn't have to swipe and the cv auto scrolls. I still need access to the targetContentOffset.pointee.y so I can do something with with the visibleItem but I the method it's in doesn't trigger on autoscroll.
How can I programmatically access the targetContentOffset.pointee.y?
You can try
let index = IndexPath(item: xyz, section: 0)
collectionView.scrollToItem(at:index, at: .centeredVertically, animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if let cell = collectionView.cellForItem(at:index) {
let frame = collectionView.convert(cell.frame,nil)
}
}
Related
I have a UIScrollView which scrolls automatically by setting its content offset within via a UIViewPropertyAnimator. The auto-scrolling is working as expected, however I also want to be able to interrupt the animation to scroll manually.
This seems to be one of the selling points of UIViewPropertyAnimator:
...dynamically modify your animations before they finish
However it doesn't seem to play nicely with scroll views (unless I'm doing something wrong here).
For the most part, it is working. When I scroll during animation, it pauses, then resumes once deceleration has ended. However, as I scroll towards the bottom, it rubber bands as if it is already at the end of the content (even if it is nowhere near). This is not an issue while scrolling towards the top.
Having noticed this, I checked the value of scrollView.contentOffset and it seems that it is stuck at the maximum value + the rubber banding offset. I found this question/answer which seems to be indicate this could be a bug with UIViewPropertyAnimator.
My code is as follows:
private var maxYOffset: CGFloat = .zero
private var interruptedFraction: CGFloat = .zero
override func layoutSubviews() {
super.layoutSubviews()
self.maxYOffset = self.scrollView.contentSize.height - self.scrollView.frame.height
}
private func scrollToEnd() {
let maxOffset = CGPoint(x: .zero, y: self.maxYOffset)
let duration = (Double(self.script.wordCount) / Double(self.viewModel.wordsPerMinute)) * 60.0
let animator = UIViewPropertyAnimator(duration: duration, curve: .linear) {
self.scrollView.contentOffset = maxOffset
}
animator.startAnimation()
self.scrollAnimator = animator
}
extension UIAutoScrollView: UIScrollViewDelegate {
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
// A user initiated pan gesture will begin scrolling.
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
self.interruptedFraction = scrollAnimator.fractionComplete
scrollAnimator.pauseAnimation()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if let scrollAnimator = self.scrollAnimator, self.viewModel.isScrolling {
scrollAnimator.startAnimation()
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
switch scrollView.panGestureRecognizer.state {
case .changed:
// A user initiated pan gesture triggered scrolling.
if let scrollAnimator = self.scrollAnimator {
let fraction = (scrollView.contentOffset.y - self.maxYOffset) / self.maxYOffset
let boundedFraction = min(max(.zero, fraction), 1)
scrollAnimator.fractionComplete = boundedFraction + self.interruptedFraction
}
default:
break
}
}
}
Is there anywhere obvious I'm going wrong here? Or any workarounds I can employ to make the scroll view stop rubber banding on scroll downwards?
You can add tap Gesture Recognizer and call this function,
extension UIScrollView {
func stopDecelerating() {
let contentOffset = self.contentOffset
self.setContentOffset(contentOffset, animated: false)
}
}
I'm trying to do pull to refresh and infinite scrolling to table view without the need for external libraries
I was setting the delegate correctly:
tableView.delegate = self
But when scrolling inside tableview, scroll methods do not respond to scrolling action for example:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if scrollView == self.tableView {
let currentOffset: Float = Float(scrollView.contentOffset.y)
if currentOffset < 25 {
//refresh content
}
let offsetY = tableView.contentOffset.y
let contentHeight = tableView.contentSize.height
if offsetY > contentHeight - scrollView.frame.size.height + 25 {
// load more
}
}
}
This case happened when I changed the scheme build configuration to "Release", but when getting back to "Debug" it's working correctly.
My deployment target is: 11.0, and XCode Version 10.2 (10E125)
After a lot of search it might be a Swift 5 compiler problem (I found a similar bug reported there):
The solution is to add #objc to each method:
#objc func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if scrollView == self.tableView {
let currentOffset: Float = Float(scrollView.contentOffset.y)
if currentOffset < 25 {
//refresh content
}
let offsetY = tableView.contentOffset.y
let contentHeight = tableView.contentSize.height
if offsetY > contentHeight - scrollView.frame.size.height + 25 {
// load more
}
}
}
I am using swift 3 to write an app which has a UIViewController looks like the following picture. It contains a horizontal-scrollable collectionView (the blue block) on the top, and its functionality is to switch between different labels(first, prev, current, next, last...etc, the total amount of labels are not fixed). What I need to achieve is that on scrolling (or panning) the collection view, the 'NEXT' or 'PREV' should move (and anchor automatically) to the center with velocity similar to paging animation. How could I achieve this? Currently I use the Paging Enabled attribute of the scrollView, however this only work when there is one label in the window.
Similar features may look like the middle "albums" section of iTunes app, which the collectionView cell would scroll to some pre-defined point automatically with some animation once it detects any swipe/pan gesture.
according to your comment if you want to do the Tap to select thing there are 2 things you need to do
1- Disable Collection View Scrolling in viewDidLoad
collectionView.isScrollingEnabled = false
2- Display Selected Cell in Center , add this in your didSelectItemAtIndexPath method
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collection.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizantally)
let cell = collectionView.cellForItem(at : indexPath) as! YourCollectionViewCellClass
cell.titleLable.font = UIFont.boldSystemFont(ofSize : 20)
}
Thanks to Raheel's answer here, I finally find a way for a desirable scroll effect. Some key functions are listed as follow in swift:
First, disable Paging Enabled attribute of the scrollView
Define setIdentityLayout() and setNonIdentityLayout() which defines the layouts of both selected / non-selected cell (for example, MyCollectionViewCell in this case)
Define some related properties such as:
lazy var COLLECTION_VIEW_WIDTH: CGFloat = {
return CGFloat(self.collectionView.frame.size.width / 2)
}()
fileprivate let TRANSFORM_CELL_VALUE = CGAffineTransform(scaleX: 1, y: 1) // Or define other transform effects here
fileprivate let ANIMATION_SPEED = 0.2
implement the following key methods for UIScrollViewDelegate:
func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
// Do tableView data refresh according to the corresponding pages here
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Scroll to corresponding position
let pageWidth: Float = Float(COLLECTION_VIEW_WIDTH) // width + space
let currentOffset: Float = Float(scrollView.contentOffset.x)
let targetOffset: Float = Float(targetContentOffset.pointee.x)
var newTargetOffset: Float = 0
if targetOffset > currentOffset {
newTargetOffset = ceilf(currentOffset / pageWidth) * pageWidth
}
else {
newTargetOffset = floorf(currentOffset / pageWidth) * pageWidth
}
if newTargetOffset < 0 {
newTargetOffset = 0
}
else if (newTargetOffset > Float(scrollView.contentSize.width)){
newTargetOffset = Float(Float(scrollView.contentSize.width))
}
targetContentOffset.pointee.x = CGFloat(currentOffset)
scrollView.setContentOffset(CGPoint(x: CGFloat(newTargetOffset), y: scrollView.contentOffset.y), animated: true)
// Set transforms
let identityIndex: Int = Int(newTargetOffset / pageWidth)
var cell = delegate!.roomCollections.cellForItem(at: IndexPath.init(row: identityIndex, section: 0)) as? MyCollectionViewCell
UIView.animate(withDuration: ANIMATION_SPEED, animations: {
cell?.transform = CGAffineTransform.identity
cell?.setIdentityLayout()
})
// right cell
cell = delegate!.roomCollections.cellForItem(at: IndexPath.init(row: identityIndex + 1, section: 0)) as? MyCollectionViewCell
UIView.animate(withDuration: ANIMATION_SPEED, animations: {
cell?.transform = self.TRANSFORM_CELL_VALUE
cell?.setNonIdentityLayout()
})
// left cell, which is not necessary at index 0
if (identityIndex != 0) {
cell = delegate!.roomCollections.cellForItem(at: IndexPath.init(row: identityIndex - 1, section: 0)) as? MyCollectionViewCell
UIView.animate(withDuration: ANIMATION_SPEED, animations: {
cell?.transform = self.TRANSFORM_CELL_VALUE
cell?.setNonIdentityLayout()
})
}
}
Finally, define initial layout in func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath):
if (indexPath.row == 0 && /* other first item selected condition */) {
cell.setIdentityLayout()
} else {
cell.transform = TRANSFORM_CELL_VALUE
cell.setNonIdentityLayout()
}
To add touch scroll effect, simply add target to each collection views, and do similar calculation which changes the contentOffSet of the scrollView, which is omitted here because this feature is simpler and not the primary question.
How do I get the UIScrollView to stop dragging at the end of the view, and snap it into a correct position? I've attempted using the following code, but while it correctly decelerates, and snaps into the perfect position at the top of the view, it does not allow me to scroll down:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let layout = self.collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
let itemHeightIncludingSpacing = self.view.frame.height + layout.minimumInteritemSpacing
var offset = targetContentOffset.memory
let index = (offset.x + scrollView.contentInset.bottom) / itemHeightIncludingSpacing
let roundedIndex = round(index)
offset = CGPoint(x: roundedIndex * itemHeightIncludingSpacing - scrollView.contentInset.bottom, y: -scrollView.contentInset.top)
targetContentOffset.memory = offset
}
here is the top of my scroll view:
here is the the bottom, its not in the position its supposed to be as you can see. it should be flush with the bottom offset value just like the top:
how do i get the scroll to stop flush just like the top?
Did you make an extension of UIScrollView that has that method? It doesn't exist but scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) does. If this is your custom method in the extension, make sure it is called somewhere in your code (like the delegate method I mentioned). Also, resetting the parameter is doing nothing to the scrollView. I would suggest a different approach.
I'm not sure what you're asking here either. Are you not wanting the content to be pulled past the edge of the screen before it bounces back in place?
If so, you don't need that method. Just add self.myScrollView.bounces = false to viewDidLoad()
Edit:
Ok try this
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
return UIEdgeInsetsMake(0, 0, 0, 0)
}
I was wondering how to detect if the UITableView is scrolled (up or down).
I want to hide the keyboard when the UITableView is scrolled with self.view.endEditing(true).
Thanks in advance
You can set property of UITable view (XCode 7+)
In Storyboard:
in Code:
tableView.keyboardDismissMode = .onDrag
You can add UIScrollViewDelegate. After that you can implement scrollViewDidScroll method.
I believe the complete solution would be the following:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == feedTableView {
let contentOffset = scrollView.contentOffset.y
print("contentOffset: ", contentOffset)
if (contentOffset > self.lastKnowContentOfsset) {
print("scrolling Down")
print("dragging Up")
} else {
print("scrolling Up")
print("dragging Down")
}
}
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if scrollView == feedTableView {
self.lastKnowContentOfsset = scrollView.contentOffset.y
print("lastKnowContentOfsset: ", scrollView.contentOffset.y)
}
}
The previous answers weren't 100% accurate.
Explanation: scrollViewDidEndDragging will be called when the scrolling stops, therefore we save the last know offset. After that we compare it with the current offset in the delegate method scrollViewDidScroll.
override func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
if(velocity.y>0){
NSLog("dragging Up");
}else{
NSLog("dragging Down");
}
}