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.
Related
So I'm trying to animate an UILabel to match another UILabel size and position. At first I was only working with the animation of the position part using constraints like this:
private lazy var constraintsForStateA: [NSLayoutConstraint] = [
firstLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 10),
firstLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
]
private lazy var constraintsForStateB: [NSLayoutConstraint] = [
firstLabel.leadingAnchor.constraint(equalTo: secondLabel.leadingAnchor),
firstLabel.topAnchor.constraint(equalTo: secondLabel.topAnchor)
]
So I basically have two arrays with these constraints above (constraintsForStateA and constraintsForStateB), and when I need to animate the position of my firstLabel I just do something like:
// From state A to state B
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
NSLayoutConstraint.deactivate(self._constraintsUnselected)
NSLayoutConstraint.activate(self._constraintsSelected)
self.layoutIfNeeded()
} completion: { _ in
self.firstLabel.alpha = 0
self.secondLabel.alpha = 1
}
}
So far this has been working exactly as I was expecting, but the part with the size is giving me some trouble, since I can't apply the same strategy to the text size I'm using a transform like this:
let scaleX = secondLabel.bounds.width / firstLabel.bounds.width
let scaleY = secondLabel.bounds.height / firstLabel.bounds.height
and then in the same .animate method above from state A to state B I do this:
firstLabel.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
and from state B to state A:
firstLabel.transform = .identity
Which works as I want but the problem I'm facing is that the position is no longer in the expected place. I think this is happening because the transformation is happening having in consideration the anchorPoint at the center of the label. I've tried sort of blindly making it work changing the anchorPoint to be at (0,0), but it's not working either. I've lost 2 days on this already, any help or guidance is welcome!
When you animate, it would be much simpler if you just forget about the constraints and just deal with frames:
NSLayoutConstraint.deactivate(self.constraintsForStateA)
firstLabel.translatesAutoresizingMaskIntoConstraints = true
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
firstLabel.frame = secondLabel.frame
}
Then to transition from state B to A:
firstLabel.translatesAutoresizingMaskIntoConstraints = false
UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseOut) {
NSLayoutConstraint.activate(self.constraintsForStateA)
self.layoutIfNeeded()
}
I try to rotate a uiview (height & width of 100) with an angle of 90 degrees with an own anchor point. After changing the anchor point I translate my view so that both changes cancel each other out.
When I rotate the view with (Double.pi) it animates as expected but when I change to (Double.pi/2) the view is jumping up.
testView.layer.anchorPoint = CGPoint(x: 0.5, y: 1)
testView.layer.transform = CATransform3DMakeTranslation(0, -50, 0)
UIView.animate(withDuration: 2.3, delay: 0, options: .curveLinear, animations: { () -> Void in
var allTransofrom = self.testView.layer.transform
var c3d = CATransform3DIdentity
c3d.m34 = 2.5 / (2000)
let rot = CATransform3DRotate(c3d, CGFloat(Double.pi/2), 1, 0, 0)
allTransofrom = CATransform3DConcat(allTransofrom, rot)
self.testView.layer.transform = allTransofrom
}, completion: {(finished) in
} )
full code here
I was able to found a solution by myself but
I still don't know where the jump is coming from.
Instead of doing a translation via CGAffineTransform.translatedBy or CATransform3DMakeTranslation I simply adapted my NSLayoutConstraint.
testViewAnchors[0].constant = testViewAnchors[0].constant + CGFloat(50)
In my case an animation for the "translation" wasn't necessary but as it is possible to animate NSLayoutConstraints it shouldn't be a problem.
I have a weird problem that seems to be fairly easy to solve. I think I could use some workaround to be able to have the behavior that I want but I want to know if there is a better way to do it.
I have a view called contentView (blue in the image) that it will expand its height using a UIView.animation, this is no problem at all and works as expected.
The problem here is that this view has a child component (a button) that has an autolayout constraint to its bottom equal to 22 like this:
This is the autolayout constraint:
If I do the height resize without the animation it works fine, the view height change and the button are always 22 points of the bottom of the contentView. But when I use an animation to make this change more smoothy and user-friendly the button moves to the end position before the animation even start.
I want to know how I could achieve a smooth animation here but with the button moving along its parent view
The part of the code that handles the animation is pretty straightforward but I'll post it in here:
#IBAction func openDetail(_ sender: ExpandCourseDetail) {
let rotation = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 0.5, delay: 0.1, options: [.curveEaseInOut], animations: {
sender.transform = rotation
}, completion: {
success in
sender.setOpen(!sender.getOpen())
})
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
self.contentView.frame.size.height = sender.getOpen() ? self.contentView.frame.height - 300 : self.contentView.frame.height + 300
}, completion: nil)
}
As a side note, the button itself has an animation that rotates the button 180 degrees to show the user that the view is expanding.
Thank you so much for your help.
It's super easy with constraints, just create a superView height constraint IBOutlet and change its constant value.
#IBAction func btnPressed(_ sender: UIButton) {
self.toggleButton.isSelected = !sender.isSelected
//Animation starts here
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.7) {
if self.toggleButton.isSelected {
//transform button
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
//change your ParentView height to desired one
self.constContentViewHeight.constant = self.view.frame.size.height - 220
self.view.layoutIfNeeded()
} else {
self.toggleButton.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi*2))
// Set height constraint to original value
self.constContentViewHeight.constant = 250
self.view.layoutIfNeeded()
}
}
}
I have created a demo, check it out.
The issue you are facing is due to two animation blocks. So I have changed some lines and put both button transformation and height animation into one animation block.
func openDetail(_ sender: ExpandCourseDetail) {
let isOpen = sender.getOpen() ? CGAffineTransform.identity : CGAffineTransform(rotationAngle: CGFloat.pi)
UIView.animate(withDuration: 1.0, delay: 0.5, options: [.curveEaseInOut], animations: {
sender.transform = rotation
self.contentView.frame.size.height = self.contentView.frame.height + (isOpen ? 300 : -300)
}, completion: { (success) in
sender.setOpen(!isOpen)
})
}
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.
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]