Scrolling UICollectionView to specific point upon letting go of scrolling - ios

When the user lets go of scrolling in a UICollectionView, I want it to scroll to a relevant point. In this example, I'll use CGPoint(x: 0, y: 100).
I've previously tried doing this:
func scrollViewWillBeginDecelerating(scrollView: UIScrollView) {
self.collectionView.setContentOffset(CGPoint(x: 0, y: 100), animated: true)
}
But that gives an animation that starts with a CurveEaseIn. If the user were to let go as they were dragging, this would give an odd appearance as the dragging suddenly halts as they lift their finger, then starts moving again.
So I tried doing this with my own animation instead:
UIView.animateWithDuration(
0.3,
delay: 0,
options: UIViewAnimationOptions.CurveEaseOut,
animations: {
() -> Void in
self.collectionView.setContentOffset(CGPoint(x: 0, y: 100), animated: false)
}, completion: nil)
The animation take place just like I'd want, but the cells which would be off screen at the end of the animation disappear immediately. The same thing happens if I replace setContentOffset() with contentOffset =.
So then somebody suggest that instead, I use the scrollViewWillEndDragging:withVelocity:targetContentOffset: method. So I did:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
targetContentOffset.memory = CGPoint(x: 0, y: 100)
}
What happens now is the animation occurs with the given velocity, so if the user lifts their finger, then it slowly makes its way to the desired location, with 0 velocity.

Related

Uitableview shift down

I've two tableviews side by side, I use below code to make them scroll like one table when I scroll on either of the two tables.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.coinNameTableView {
self.coinInfoTableView?.setContentOffset(CGPoint.init(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y), animated: false)
}
if scrollView == self.coinInfoTableView {
self.coinNameTableView?.setContentOffset(CGPoint.init(x: scrollView.contentOffset.x, y: scrollView.contentOffset.y), animated: false)
}
}
But there is one issue, when the first cell is at the begin of the table, then I scroll down, the whole table will shift down and not bounce back to the original location.
In the scrollViewDidScroll method, write logic for content offset. If content offset is less than 0, than set the content offset == 0.
Reference code:-
if scrollView.ContentOffset.y < 0{
self.coinInfoTableView?.setContentOffset(CGPoint.init(x: 0, y: 0), animated: false)
self.coinNameTableView?.setContentOffset(CGPoint.init(x: 0, y: 0), animated: false)
}

UIScrollView contentOffset animates to another point

Good day,
I'm facing a wired behavior in a UIScrollView here. I'm implementing a custom circular UIScrollView and in a point I've to set the origin of the scroll view to a point
scrollView.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
After that I change the content offset using animation to a specific point
scrollView.setContentOffset(CGPoint(x: point, y: 0), animated: true)
The problem is that although point is correct (ex. 400) scrollview sometimes jumps to another point so code like the following should print 10 if point = 10 but sometimes it prints other values.
scrollView.setContentOffset(CGPoint(x: point, y: 0), animated: true)
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
print("Current x: \(self.tabsScrollView.contentOffset.x)")})
Although I made sure that no other animation was in progress.
I found that someone was suggesting something like that
UIView.animate(withDuration: 0.5, animations: {
self.scrollView.contentOffset.x = point
})
And surprisingly it did work, but not animated like default.
Any ideas?

How to control the speed of UIView animation?

I am implementing a rotating digit view like this:
https://github.com/jonathantribouharet/JTNumberScrollAnimatedView
But, instead of scrolling from say 1 to 9, I want the animation to continue scrolling (1 to 9 then back to 1 to 9), until I manually stop it.
I implement this by adding UILabel subviews 1-9, vertically stacked, and add an extra label for number 1 at the bottom. When rotating to the bottom (showing the bottom label 1), in the animation completion closure, I set the transform back to origin, showing top label 1, and restart rotating animation.
This works fine, but since each rotation (1-9-1) is separate, the animation from 1-9 accelerates and then stop at 1, and pick up speed at the next iteration. This is not what I want, I want the rotation to maintain a constant speed between the iterations.
How can I do this with UIView animation?
Here is the code:
public func startAnimation(){
UIView.setAnimationCurve(UIViewAnimationCurve.easeIn)
UIView.animate(withDuration: TimeInterval(self.loopDuration), animations: { [unowned self] in
self.container!.transform = CGAffineTransform(translationX: 0, y: CGFloat(-(self.endNumber-self.startNumber+1)*(self.size)))
}) { [unowned self] (completed) in
if self.isAnimationStopped {
self.container!.transform = CGAffineTransform(translationX: 0, y: 0)
UIView.setAnimationCurve(UIViewAnimationCurve.easeOut)
UIView.animate(withDuration: TimeInterval(self.loopDuration), animations: {
self.container!.transform = CGAffineTransform(translationX: 0, y: CGFloat(-self.targetY))
})
} else {
self.container!.transform = CGAffineTransform(translationX: 0, y: 0)
self.startAnimation()
}
}
}
By default UIView animation uses ease-in, ease-out animation timing, where the animation accelerates smoothly, runs, then decelerates to a stop.
You need to use a longer form of UIView.animate, animate(withDuration:delay:options:animations:completion:) that takes an options parameter, and specify a timing curve of .curveLinear.
Just specify 0.0 for the delay and options: .curveLinear
Edit:
Note that if you need to specify multiple options, like .curveLinear and .allowUserInteraction you would use OptionSet syntax, like options: [.curveLinear, .allowUserInteraction]

UIScrollView behaving weird when updating constraints subview

I made an UIScrollView in a XIB for my onboarding. The UIScrollView has 3 onboarding views. Long story short:
This works perfect. However I want the top left and right buttons (Overslaan - Volgende) to animate up / off the screen when the third/last page is on screen. My UIScrollView starts behaving weird when I animate the buttons off:
This is the code im using:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.frame.width)
pageControl.currentPage = pageIndex
if stepViews[pageIndex] is OnboardingLoginView {
moveControlConstraintsOffScreen()
} else {
moveControlConstraintsOnScreen()
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.layoutIfNeeded()
}, completion: nil)
}
I debugged the code and it turns out that setting a new constant for the constraints causes the issue, regardless of the animation block. How do I make the buttons move up/off the screen without my scrollView behaving weird?
It looks like triggering a layout pass is interfering with your scroll view positioning. You could implement func scrollViewDidScroll(_ scrollView: UIScrollView) and try to update the button view on a per-frame basis, without changing Auto Layout constraints. I usually use the transform property for frame changes outside of Auto Layout, since any changes to frame or bounds are overwritten during the next layout pass.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
guard mustBeOnLastPage else {
buttonOne.tranform = .identity
buttonTwo.tranform = .identity
return
}
let offset = scrollView.contentOffset.x
buttonOne.tranform = .init(translationX: 0, y: offset)
buttonTwo.tranform = .init(translationX: 0, y: offset)
}
Old answer
This interpretation is a bit of tangent of what you're asking for.
Since it looks like you're using the scroll view in a paging context, I would approach this problem by using UIPageViewController. Since UIPageViewController uses UIScrollView internally, you can observe the contentOffset of the last view in the scroll view to determine how far along the page has scrolled.
Yes, this involves looking inside the view hierarchy, but Apple hasn't changed it for half a decade so you should be safe. Coincidentally, I actually implemented this approach last week and it works like a charm.
If you're interested, I can further expand on this topic. The post that pointed me in the right direction can be found here.
Here is what i do
I can't post image,you can look at this http://i.imgur.com/U7FHoMu.gif
and this is the code
var centerYConstraint: Constraint!
func setupConstraint() {
fadeView.snp.makeConstraints { make in
make.centerX.equalToSuperview()
centerYConstraint = make.centerY.equalToSuperview().constraint
make.size.equalTo(CGSize(width: 50, height: 50))
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint,targetContentOffset: UnsafeMutablePointer<CGPoint>) {
let pageIndex = Int(targetContentOffset.pointee.x / self.view.frame.width)
if pageIndex != 1 {
centerYConstraint.update(offset: self.view.frame.height)
} else {
centerYConstraint.update(offset: 0)
}
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Based on #CloakedEddy's answer I made 2 changes:
1: It seems layoutSubviews is responsible for the weird behaviour. To fix this I prevent the scrollView from calling layoutSubviews all the time:
override func layoutSubviews() {
super.layoutSubviews()
if !didLayoutSubviews {
for index in 0..<stepViews.count {
let page: UIView = stepViews[index]
let xPosition = scrollView.frame.width * CGFloat(index)
page.frame = CGRect(x: xPosition, y: 0, width: scrollView.bounds.width, height: scrollView.frame.height)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(index + 1)
}
didLayoutSubviews = true
}
}
2: If you want to update your views for device orientations:
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
onboardingView.didLayoutSubviews = false
onboardingView.setNeedsLayout()
}

Curious effect when animating UIViews

I'm making squares appear at the top of the screen, run till they reach the bottom and then disappear. This is done adding a new UIView to the ViewController's view and then, after the animation is completed, removing it from the subviews array. To add some beauty to all of this, I apply a random rotation using a CGAffineTransform at the moment of their creation.
The curious thing here is that when they are moving, they are being scaled randomly! I know this must be related to the rotation, because if I don't apply it, all the squares move perfectly without scaling, but when I apply it, it happens, so...
I hope someone could test it and tell me what's happening... Why are squares scaling???
This is the code:
import UIKit
class ViewController: UIViewController {
var timer: Timer!
override func viewDidLoad() {
timer = Timer.scheduledTimer(
timeInterval: 1,
target: self,
selector: #selector(createView),
userInfo: nil,
repeats: true
)
}
#objc private func createView () {
let v = UIView(frame: CGRect(
x: self.view.bounds.width / 2 - 50,
y: -100,
width: 100,
height: 100
))
v.backgroundColor = UIColor.red
v.transform = CGAffineTransform(
rotationAngle: CGFloat(Double(arc4random_uniform(360)) * M_PI / 180)
)
self.view.addSubview(v)
UIView.animate(withDuration: 5, delay: 0, options: [.curveLinear], animations: {
v.frame.origin.y = self.view.bounds.height
}, completion: {
_ in
v.removeFromSuperview()
})
print(self.view.subviews.count)
}
}
The first rule of animation club is don't manipulate a view's frame when you've changed it's transform.
If a view's transform is not the identity transform then the frame is undefined, and trying to change the frame will result in undefined behavior. It sounds like you're experiencing some of that undefined behavior.
You should refactor your code to manipulate the view's center property rather than the frame, or apply your offset using the transform, but that is tricky since rotation changes the frame of reference for the offset.
I recommend adjusting the view's center property.

Resources