UIScrollView behaving weird when updating constraints subview - ios

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

Related

How to animate image change in ScrollView Swift

i have a scrollview that contains an array of image, I would like to animate it changing image from right to left. My scrollView:
#IBOutlet weak var scrollView: UIScrollView!
var imageArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
scrollView.frame = view.frame
imageArray = [UIImage(named:"image3")!,UIImage(named:"image4")!,UIImage(named:"image1")!]
for i in 0..<imageArray.count{
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.image = imageArray[i]
let xPosition = self.view.frame.width * CGFloat(i)
imageView.frame = CGRect(x: xPosition, y: 0, width: self.scrollView.frame.width, height: 205)
scrollView.contentSize.width = scrollView.frame.width * CGFloat(i + 1)
scrollView.addSubview(imageView)
}
startAnimating()
}
and for animation I did it with :
func startAnimating() {
UIView.animateKeyframes(withDuration: 2, delay: 0, options: .repeat, animations: {
self.scrollView.center.x += self.view.bounds.width
}, completion: nil)
}
However, it is moving from left to right and not right to left, plus it's not changing images...How should i go about this? Any guidance is much appreciated! Thank you
You're moving the scrollview within it's parent view. The image views are children of the scrollview (they're inside the scrollview), so they're simply moving with it.
To scroll the content within your UIScrollView you should be using its contentOffset property:
var newOffset = scrollView.contentOffset
newOffset.x += scrollView.frame.size.width
UIView.animate(withDuration: 2, delay: 0, options, .repeat, animations: {
self.scrollView.contentOffset = newOffset
})
Also, you shouldn't be using animateKeyFrames for this. Use the animate(withDuration:delay:options:animations) method instead.
It's also worth thinking about whether a UIScrollView is the right choice here, and it really comes down to how many images are going to inside it.
If it's always going to be a small number of images, then a scrollview is a fine choice.
However, if there are ever going to be a larger number images to display, then UICollectionView would be a better choice as it reuses its subviews.

Can I stop UIScrollView at scrolled position?

I want to stop UIScrollView scrolling when it reached at specific point.
I thought it will be solve by below code.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let scrollY: CGFloat = scrollView.contentOffset.y
stopScrollViewIfNeeded(by: scrollY)
}
func stopScrollViewIfNeeded(by scrollY: CGFloat) {
guard scrollY <= SpecificY else {
return
}
scrollView.isScrollEnabled = false
}
But I was wrong. because contentOffset.y be to 0 when scroll be disabled.
And I was improved my function like below code, but it still didn't work as I wanted.
func stopScrollViewIfNeeded(by scrollY: CGFloat) {
guard scrollY <= SpecificY else {
return
}
scrollView.setContentOffset(.init(x: 0, y: SpecificY), animated: false)
scrollView.isScrollEnabled = false
scrollView.isPagingEnabled = true
}
How do I improve stopScrollViewIfNeeded() to work as I wanted?
just set contentOffset as:
Just need to get the offset value at which you want it to be stopped and Assign values as Below
Horizontal ScrollView
UIView.animate(withDuration: 0.5) {
self.MainScollView.contentOffset.x = self.view.frame.size.width*2
}
or Vertical scrollView
UIView.animate(withDuration: 0.5) {
self.MainScollView.contentOffset.y = 90
}
You can override the scroll position in didScroll. Logic in the didScroll can effectively make the scroll stick for as long as you want.

Animate when scrolling using UIScrollView SWIFT

Im having abit of trouble animating a label to move into another position when a user scrolls horizontally.
Here is the snippet i have:
override func viewDidLoad() {
super.viewDidLoad()
buttonTextSpacing(UIButton: vysionButton, lineSpacing: 2.5, UIColor: .black)
buttonTextSpacing(UIButton: converseButton, lineSpacing: 2.5, UIColor: .black)
buttonTextSpacing(UIButton: storiesButton, lineSpacing: 2.5, UIColor: .black)
self.storiesCollectionView.delegate = self
self.storiesCollectionView.dataSource = self
// Scroll View
self.scrollView.contentSize = CGSize(width: self.scrollView.frame.width*2, height: self.scrollView.frame.height)
self.scrollView.addSubview(storiesCollectionView)
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.delegate = self
}
func scrollViewDidScroll(scrollUIView: UIScrollView) {
print("scrollViewDidScroll")
DispatchQueue.main.async {
UIView.animate(withDuration: 0.4, delay: 0, options: UIViewAnimationOptions.curveEaseOut, animations: {
self.scrollLabel.center.x = self.storiesButton.center.x - 1
}, completion: nil)
}
}
Basically what im trying to achieve is that when a user scrolls, the animation starts but does not finish until the scroll is finished to the next custom uiview (I have paging enabled also).
Currently it doesnt do that, im not able to call the scrolling event nor even make the label animate (To the new position) while scrolling.
Many thanks in advance for any advice or help.
Cheers,
Pon
As I understand you have some label as subview on scrollView and that label is located over some views that you scroll. You may use scrollView.contentOffset.x instead of animation. For example: You need to calculate the distance between the start position of label and end position. That distance divide by 100, you get step (1 percent of distance width) that you need to multiply by scrollView.contentOffset.x. Give more details of your question.

Animate UICollectionViewLayout header size causes unwanted flash cross dissolve

Vis of problem: http://i.imgur.com/TastPR9.gifv
func animateHeaderResize(height: CGFloat) {
let layout = self.collectionView?.collectionViewLayout as! UICollectionViewFlowLayout;
layout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.width, height: height);
UIView.animate(withDuration: 1, delay: 0, options: .curveEaseOut, animations: {
self.collectionView?.layoutIfNeeded();
}) { (finished) in
//self.loadPosts();
}
}
I am attempting to animate a change in the header size of a UICollectionView through it's UICollectionViewLayout. The resulting animation has a weird flash and stretch that might be a cross dissolve transition being triggered somewhere. I have tried many variations of setting the layout of the collection view including setCollectionViewLayout(layout:animated:) and implementing collectionView(_ collectionView: layout: referenceSizeForHeaderInSection section:) but all result in the same animation.
I have noticed that the same flash occurs when changing the item size of the collection view. Maybe this is some default behavior I can modify? Do I have to subclass UICollectionViewFlowLayout? I have tried searching and can't find a solution, would appreciate just a step in the right direction.
This ended up working for me:
func animateHeaderResize(height: CGFloat) {
self.collectionView?.performBatchUpdates({
let layout = self.collectionView?.collectionViewLayout as! UICollectionViewFlowLayout;
layout.headerReferenceSize = CGSize(width: UIScreen.main.bounds.width, height: height);
}, completion: { (fin) in
self.loadPosts();
});
}
Saw the same issue and I could see that animation looks better if i set:
layout.sectionHeadersPinToVisibleBounds = false

Scrolling UICollectionView to specific point upon letting go of scrolling

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.

Resources