I have a segmented control and I'd like to animate a UIView to rotate clockwise when the right side of the control is tapped, and counter-clockwise when the left side is tapped. My code to rotate is:
func rotateRight() {
UIView.animate(withDuration: 0.5, animations: {
self.profileImageView.transform = CGAffineTransform(rotationAngle: (180.0 * CGFloat(M_PI)) / 180.0)
self.profileImageView.transform = CGAffineTransform(rotationAngle: (0.0 * CGFloat(M_PI)) / 360.0)
})
And then I'm using this code to handle the changes depending on which segment is tapped (Edited for update):
if loginRegisterSegmentedControl.selectedSegmentIndex == 0 {
self.loginRegisterButton.backgroundColor = UIColor.blue
loginRegisterSegmentedControl.tintColor = UIColor.blue
rotateLeft()
// Roll to the left
} else {
self.loginRegisterButton.backgroundColor = UIColor.red
loginRegisterSegmentedControl.tintColor = UIColor.red
rotateRight()
// Roll to the right
}
So basically in the if conditional I want to call a rotateLeft() function - how can I do this?
EDIT: Here's my full code for the animation, including Pierce's suggestion:
// Rotation
var viewAngle: CGFloat = 0 // Right-side up to start
let π = CGFloat.pi // Swift allows special characters hold alt+p to use this, it allows for cleaner code
func rotate(by angle: CGFloat) {
self.viewAngle += angle
UIView.animate(withDuration: 0.5, animations: {
self.profileImageView.transform = CGAffineTransform(rotationAngle: self.viewAngle)
self.view.layoutIfNeeded()
})
}
lazy var profileImageView: UIImageView = {
let imageView = UIImageView()
imageView.image = UIImage(named: "TTTdude")
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(rotate)))
imageView.isUserInteractionEnabled = true
return imageView
}()
lazy var loginRegisterSegmentedControl: UISegmentedControl = {
let sc = UISegmentedControl(items: ["iOS", "Android"])
sc.translatesAutoresizingMaskIntoConstraints = false
sc.tintColor = UIColor.black
sc.selectedSegmentIndex = 0
sc.addTarget(self, action: #selector(handleLoginRegisterChange), for: .valueChanged)
return sc
}()
func handleLoginRegisterChange() {
let title = loginRegisterSegmentedControl.titleForSegment(at: loginRegisterSegmentedControl.selectedSegmentIndex)
loginRegisterButton.setTitle(title, for: .normal)
if loginRegisterSegmentedControl.selectedSegmentIndex == 0 {
self.loginRegisterButton.backgroundColor = UIColor.blue
loginRegisterSegmentedControl.tintColor = UIColor.blue
rotate(by: -2*π)
// Roll to the left
} else {
self.loginRegisterButton.backgroundColor = UIColor.red
loginRegisterSegmentedControl.tintColor = UIColor.red
rotate(by: 2*π)
// Roll to the right
}
Just rotate it by whatever angle you did for clockwise, but multiply it by negative one
func rotateLeft() {
UIView.animate(withDuration: 0.5, animations: {
self.profileImageView.transform = CGAffineTransform(rotationAngle: ((180.0 * CGFloat(M_PI)) / 180.0) * -1)
self.profileImageView.transform = CGAffineTransform(rotationAngle: ((0.0 * CGFloat(M_PI)) / 360.0) * -1)
self.view.layoutIfNeeded()
})
}
I should also mention that when you say (180.0 * CGFloat(π)) / 180 - You're just saying π, because 180/180 is one.
It looks to me like you're trying to do two different rotations at once? I'm a little confused by that. One thing I've found that really helps is to track the view's axis orientation if you're doing multiple rotations both clockwise and counter-clockwise. Do something like create a property called viewAngle
var viewAngle: CGFloat = 0 // Right-side up to start
let π = CGFloat.pi // Swift allows special characters hold alt+p to use this, it allows for cleaner code
func rotate(by angle: CGFloat) {
for i in 0 ..< 4 {
UIView.animate(withDuration: 0.125, delay: 0.125 * Double(i), options: .curveLinear, animations: {
self.viewAngle += angle/4
self.rotateView.transform = CGAffineTransform(rotationAngle: self.viewAngle)
self.view.layoutIfNeeded()
}, completion: nil)
}
}
If you want to rotate right pass in a positive angle (i.e. π, π/2, π/4, 2*π), or if left pass in a negative angle.
Rotate right by 180 degrees:
rotate(by: π)
Rotate left by 180 degrees:
rotate(by: -π)
EDIT: As you were mentioning, which I didn't see until I tried for myself, when you plug in π or 2π whether it's negative or positive, it just rotates clockwise. To get around this I had to make it rotate by -π/4 increments. So to do a full-circle rotate counter clockwise I did a loop of -π/4 animations if that makes sense. It works, but I feel like there should be an easier way to do this. I would appreciate anyone else chiming in.
(swift 4)
it will rotate 180 degree counter clock wise
self.viewRoatate.transform = CGAffineTransform(rotationAngle: CGFloat.pi / -2)
self.viewRoatate.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
self.view.layoutIfNeeded()
func rotate(button: UIButton, time: Double, overlap: Double, clockWise: Bool) {
var angles = [CGFloat(Double.pi), CGFloat(Double.pi) * 2]
if !clockWise {
angles = [CGFloat(Double(270) * .pi/180),
CGFloat(Double(180) * .pi/180),
CGFloat(Double(90) * .pi/180),
CGFloat(Double(0) * .pi/180)]
}
for i in 0..<angles.count {
UIView.animate(withDuration: time, delay: time - overlap * Double(i), animations: {
button.transform = CGAffineTransform(rotationAngle: angles[i])
}, completion: nil)
}
By using CABasicAnimation:
left-rotation(counter-clockwise: -360) & right-rotation(clockwise: 360)
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.toValue = direction == .left ? -2 * (CGFloat.pi) : 2 * CGFloat.pi
animation.duration = 1
button.layer.add(animation, forKey: nil)
Related
My question pertains to how to mimic this Carousel view Youtube video only using a UIView not it's layer or a CALayer, which means actually transforming the UIViews its self.
I found a stack overflow question that actually is able to convert a
CATransform3D into a CGAffineTransform. That was written by some genius here as an answer, but my problem is a little unique.
The animation you see below is using CALayer to create. I need to create this same animation but transforming the UIView instead of its layer.
What it's Supposed to look like:
Code (Creates animation using Layers):
This takes an image card which is a CALayer() with a image attached to it and transforms which places it in the Carousel of images.
Note: turnCarousel() is also called when the user pans which moves / animates the Carousel.
let transformLayer = CATransformLayer()
func turnCarousel() {
guard let transformSubLayers = transformLayer.sublayers else {return}
let segmentForImageCard = CGFloat(360 / transformSubLayers.count)
var angleOffset = currentAngle
for layer in transformSubLayers {
var transform = CATransform3DIdentity
transform.m34 = -1 / 500
transform = CATransform3DRotate(transform, degreeToRadians(deg: angleOffset), 0, 1, 0)
transform = CATransform3DTranslate(transform, 0, 0, 175)
CATransaction.setAnimationDuration(0)
layer.transform = transform
angleOffset += segmentForImageCard
}
}
What It Currently Looks Like:
So basically it's close, but it seems as though there is a scaling issue with the cards that are supposed to be seen as in the front and the cards that are in the back of the carousel.
Fo this what I did is use a UIImageView as the base view for the carousel and then added more UIImageViews as cards to it. So now we are trying do a transformation on a UIImageView/UIView
Code:
var carouselTestView = UIImageView()
func turnCarouselTestCarousel() {
let segmentForImageCard = CGFloat(360 / carouselTestView.subviews.count)
var angleOffset = currentAngleTestView
for subview in carouselTestView.subviews {
var transform2 = CATransform3DIdentity
transform2.m34 = -1 / 500
transform2 = CATransform3DRotate(transform2, degreeToRadians(deg: angleOffset), 0, 1, 0)
transform2 = CATransform3DTranslate(transform2, 0, 0, 175)
CATransaction.setAnimationDuration(0)
// m13, m23, m33, m43 are not important since the destination is a flat XY plane.
// m31, m32 are not important since they would multiply with z = 0.
// m34 is zeroed here, so that neutralizes foreshortening. We can't avoid that.
// m44 is implicitly 1 as CGAffineTransform's m33.
let fullTransform: CATransform3D = transform2
let affine = CGAffineTransform(a: fullTransform.m11, b: fullTransform.m12, c: fullTransform.m21, d: fullTransform.m22, tx: fullTransform.m41, ty: fullTransform.m42)
subview.transform = affine
angleOffset += segmentForImageCard
}
}
the sub image that actually make up the Carousel are add with this function which simply goes through a for loop of image named 1...6 in my assets folder.
Code:
func CreateCarousel() {
carouselTestView.frame.size = CGSize(width: self.view.frame.width, height: self.view.frame.height / 2.9)
carouselTestView.center = CGPoint(self.view.frame.width * 0.5, self.view.frame.height * 0.5)
carouselTestView.alpha = 1.0
carouselTestView.backgroundColor = UIColor.clear
carouselTestView.isUserInteractionEnabled = true
self.view.insertSubview(carouselTestView, at: 0)
for i in 1 ... 6 {
addImageCardTestCarousel(name: "\(i)")
}
// Set the carousel for the first time. So that now we can see it like an actual carousel animation
turnCarouselTestCarousel()
let panGestureRecognizerTestCarousel = UIPanGestureRecognizer(target: self, action: #selector(self.performPanActionTestCarousel(recognizer:)))
panGestureRecognizerTestCarousel.delegate = self
carouselTestView.addGestureRecognizer(panGestureRecognizerTestCarousel)
}
The addImageCardTestCarousel function is here:
Code:
func addImageCardTestCarousel(name: String) {
let imageCardSize = CGSize(width: carouselTestView.frame.width / 2, height: carouselTestView.frame.height)
let cardPanel = UIImageView()
cardPanel.frame.size = CGSize(width: imageCardSize.width, height: imageCardSize.height)
cardPanel.frame.origin = CGPoint(carouselTestView.frame.size.width / 2 - imageCardSize.width / 2 , carouselTestView.frame.size.height / 2 - imageCardSize.height / 2)
guard let imageCardImage = UIImage(named: name) else {return}
cardPanel.image = imageCardImage
cardPanel.contentMode = .scaleAspectFill
cardPanel.layer.masksToBounds = true
cardPanel.layer.borderColor = UIColor.white.cgColor
cardPanel.layer.borderWidth = 1
cardPanel.layer.cornerRadius = cardPanel.frame.height / 50
carouselTestView.addSubview(cardPanel)
}
Purpose:
The purpose of this is that I want to build a UI that can take UIViews on the rotating cards you see, and a CALayer cannot add a UIView as a subview. It can only add the UIView's layer to its own Layer. So to solve this problem I need to actually achieve this animation with UIViews not CALayers.
I solved it the view that Appears to be behind the front most view are actually grabbing all the touch even if you touch a card right in the front the back card will prevent touches for the front card. So i made a function that can calculate. Which views are in the front. Than disable and enable touches for then. Its like when 2 cards get stacked on top of each other the card to the back will stop the card from the front from taking/userInteraction.
Code:
func DetermineFrontViews(view subview: UIView, angle angleOffset: CGFloat) {
let looped = Int(angleOffset / 360) // must round down to Int()
let loopSubtractingReset = CGFloat(360 * looped) // multiply 360 how ever many times we have looped
let finalangle = angleOffset - loopSubtractingReset
if (finalangle >= -70 && finalangle <= 70) || (finalangle >= 290) || (finalangle <= -260) {
print("In front of view")
if subview.isUserInteractionEnabled == false {
subview.isUserInteractionEnabled = true
}
} else {
print("Back of view")
if subview.isUserInteractionEnabled == true {
subview.isUserInteractionEnabled = false
}
}
}
I added this to the turn function to see if it could keep track of the first card being either in the back of the carousel or the front.
if subview.layer.name == "1" {
DetermineFrontViews(view: subview, angle: angleOffset)
}
I am making an async call whenever a button is clicked now I want an image which is like a refresh icon to be rotating or spinning until I get a response from my server. What i have now only rotates pi that's 180 and when the response arrives too the image doesn't reset. Have pasted my sample code below:
//When the update button is clicked
#objc func updateHome(){
UIView.animate(withDuration: 1) {
self.updateIcon.transform = CGAffineTransform(rotationAngle: .pi)
}
getBalances()
}
//Inside getBalances function
DispatchQueue.main.async {
if responseCode == 0 {
let today = self.getTodayString()
print("Time: \(today)")
UIView.animate(withDuration: 1) {
self.updateIcon.transform = CGAffineTransform(rotationAngle: .pi)
}
}
This code rotates your UIImageView indefinitely using CABasicAnimation:
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(.pi * 2.0)
rotateAnimation.duration = 1.0 // Change this to change how many seconds a rotation takes
rotateAnimation.repeatCount = Float.greatestFiniteMagnitude
updateIcon.layer.add(rotateAnimation, forKey: "rotate")
And when you get your signal from your server you can call
updateIcon.layer.removeAnimation(forKey: "rotate")
to stop the animation
For continuous rotation, you can use CAKeyframeAnimation too like below.
#objc func updateHome() {
let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
animation.duration = 1.0
animation.fillMode = kCAFillModeForwards
animation.repeatCount = .infinity
animation.values = [0, Double.pi/2, Double.pi, Double.pi*3/2, Double.pi*2]
let moments = [NSNumber(value: 0.0), NSNumber(value: 0.1),
NSNumber(value: 0.3), NSNumber(value: 0.8), NSNumber(value: 1.0)]
animation.keyTimes = moments
self.updateIcon.layer.add(animation, forKey: "rotate")
getBalances()
}
In your Async method (inside)DispatchQueue.main.async, you can stop like below. This will keep you to the original position of the image.
self.updateIcon.layer.removeAllAnimations()
One way you can handle this is to add an instance variable that tracks when the work is completed:
var finished: Bool = false
Then write a function that rotates the image and uses the completion handler to call itself to continue rotating:
func rotateImage() {
UIView.animate(withDuration: 1,
delay: 0.0,
options: .curveLinear
animations: {
self.updateIcon.transform = CGAffineTransform(rotationAngle: .pi)
},
completion: { completed in
if !self.finished {
self.rotateImage()
}
}
}
}
If you want to stop the animation instantly, you should set finished = true and call updateIcon.layer.removeAllAnimations()
I am trying to run an if statement to change the color of a text label in SpriteKit where the number of lives has reached a certain number. But the color is not changing from white to red.
In my Game the Lives decrease by 1 every time an Asteroid passes the Spaceship.
Right now the text label code looks like this:
livesLabel.text = "Lives: 3"
livesLabel.fontSize = 70
if livesNumber == 1 {
livesLabel.fontColor = SKColor.red
} else {
livesLabel.fontColor = SKColor.white
}
livesLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.right
if UIDevice().userInterfaceIdiom == .phone {
switch UIScreen.main.nativeBounds.height {
case 2436: // Checks if device is iPhone X
livesLabel.position = CGPoint(x: self.size.width * 0.79, y: self.size.height * 0.92)
default: // All other iOS devices
livesLabel.position = CGPoint(x: self.size.width * 0.85, y: self.size.height * 0.95)
}
}
livesLabel.zPosition = 100
self.addChild(livesLabel)
I have a function where a livesNumber if statement works fine which looks like this:
func loseALife() {
livesNumber -= 1
livesLabel.text = "Lives: \(livesNumber)"
let scaleUp = SKAction.scale(to: 1.5, duration: 0.2)
let scaleDown = SKAction.scale(to: 1, duration: 0.2)
let scaleSequence = SKAction.sequence([scaleUp, scaleDown])
livesLabel.run(scaleSequence)
if livesNumber == 0{
runGameOver()
}
}
If anyone can shed a light on this that would be great. I thought that it might have something to do with String or Int, but since this works on other functions and also I tried some String to Int conversions and it changed nothing I am no longer sure what the issue is.
As can be seen from the discussion above, the problem was that the "color label" code was run once in the initialization of the view. Therefore, when the livesNumber value was later updated, the "color label" code wasn't executed.
This can be solved in several ways. One would be to add it to the looeALife function:
func loseALife() {
livesNumber -= 1
livesLabel.text = "Lives: \(livesNumber)"
if livesNumber == 1 {
livesLabel.fontColor = SKColor.red
} else {
livesLabel.fontColor = SKColor.white
}
let scaleUp = SKAction.scale(to: 1.5, duration: 0.2)
let scaleDown = SKAction.scale(to: 1, duration: 0.2)
let scaleSequence = SKAction.sequence([scaleUp, scaleDown])
livesLabel.run(scaleSequence)
if livesNumber == 0{
runGameOver()
}
}
(maybe extract it to a separate function)
You could also add a didSet to your livesNumber and then update the value there:
var livesNumber: Int = 3 {
didSet {
livesLabel.color = livesNumber == 1 ? .red : .white
}
}
Hope that helps.
I have a music player, I want the needle Image is on the disk when the player is playing. Rotating the image 45° around the point of the the upper left corner while the player isn't playing.The constraints of the needleImgView are centerX to superView, top to superview = 40, size = (97,153)
I have googled and found the following code:
func setAnchorPoint(_ anchorPoint:CGPoint, for view:UIView){
let oldOrigin = view.frame.origin
view.layer.anchorPoint = anchorPoint
let newOrigin = view.frame.origin
var transition = CGPoint()
transition.x = newOrigin.x - oldOrigin.x
transition.y = newOrigin.y - oldOrigin.y
view.center = CGPoint(x: view.center.x - transition.x, y: view.center.y - transition.y)
}
And with the following method to rotate the image:
if !player.isPlaying {
setAnchorPoint(CGPoint(x:40 / 97.0,y:40 / 153.0), for: needleImgView) // I'm NOT SURE the 40 is right.
//needleImgView.layer.anchorPoint = CGPoint(x:0,y:0)
UIView.animate(withDuration: 0.5, animations: {
self.needleImgView.transform = CGAffineTransform(rotationAngle: CGFloat(-Double.pi / 4))
}, completion: nil)
}else{
setAnchorPoint(CGPoint(x:40 / 97.0,y:40 / 153.0), for: needleImgView)
//needleImgView.layer.anchorPoint = CGPoint(x:0,y:0)
UIView.animate(withDuration: 0.5, animations: {
self.needleImgView.transform = .identity
}, completion: nil)
}
I set setAnchorPoint(CGPoint(x:0 / 97.0,y:0 / 153.0), for: needleImgView) or needleImgView.layer.anchorPoint = CGPoint(x:0,y:0),the result is that the upper left corner point also move away.
How to solve it ? Thx.
You may try CABasicAnimation.
0.5 of x of anchor point means in the middle of width, 0 of y means top of the height.
imageView.layer.anchorPoint = CGPoint.init(x: 0.5, y: 0)
let basic = CABasicAnimation.init(keyPath: "transform.rotation")
basic.fromValue = NSNumber.init(value: 0)
basic.toValue = NSNumber.init(value: -(Double.pi / 4))
basic.fillMode = kCAFillModeForwards
basic.isRemovedOnCompletion = false
imageView.layer.add(basic, forKey: nil)
There is two way to solve it ---
1- you have to change the 40 by 0 .
setAnchorPoint(CGPoint(x:0 / 97.0,y:0 / 153.0), for: needleImgView);
Change the Code ----
setAnchorPoint(CGPoint(x:-10.0 / 97.0,y:0 / 153.0), for: YourImage)
for make same for your image change the value in X.
2- Take a Double size of image Like Clock and your pointer will be start from middle like clock's needle and you can rotate without anchor point. But it is the bad way to solve .
Try first one . it will help you, or feel free.
I have one image like drop down. Initially it will look like drop down image So when user press drop down list some option will show. So what I need is, when drop-down is true.I means when user press drop down image and when the list of option are showing down I need to show the drop down image to 180 degree.Same like when drop down is false I need to show the image as normal position.
Is this way is correct instead of using one more image? I am using swift 2.2
Updated :
#IBAction func dropBtnPress(sender: AnyObject) {
if dropDown.hidden {
dropDown.show()
UIView.animateWithDuration(0.0, animations: {
self.image.transform = CGAffineTransformMakeRotation((180.0 * CGFloat(M_PI)) / 180.0)
})
} else {
dropDown.hide()
// UIView.animateWithDuration(2.0, animations: {
// self.image.transform = CGAffineTransformMakeRotation((180.0 * CGFloat(M_PI)) / -180.0)
// })
}
}
}
To rotate an image you could use this snippet:
UIView.animateWithDuration(2, animations: {
self.imageView.transform = CGAffineTransformMakeRotation(CGFloat(M_PI))
})
You can adjust the animation seconds (currently 2).
To set the image back again use the following code:
UIView.animateWithDuration(2, animations: {
self.imageV.transform = CGAffineTransform.identity
})
Swift 4.x version:
Rotate:
UIView.animate(withDuration: 2) {
self.imageView.transform = CGAffineTransform(rotationAngle: .pi)
}
Set the image to it´s normal state:
UIView.animate(withDuration: 2) {
self.imageView.transform = CGAffineTransform.identity
}
Swift 3
UIView.animate(withDuration:0.1, animations: {
self.arrowIcon.transform = CGAffineTransform(rotationAngle: CGFloat(Double.pi))
})
First of all, if you want to rotate 180 degrees, that has to translate to radians. 180 degrees in radians is pi. 360 degrees would be 2 * pi. 180 * pi would make your animation spin around 90 times in one second and end up with the original orientation.
You can do something like this,
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = M_PI
rotationAnimation.duration = 1.0
self.arrowImageView.layer.addAnimation(rotationAnimation, forKey: nil)
hope this will help :)
if you want to rotate image to fit language direction when changing language you can make your image assets direction (Left to Right,Mirrors) it works perfect with me from English to arabic You can see it how to do it here