I'm having trouble getting this CA animation to work - ios

I'm trying to make an animation where the color red pulses on the screen and when the screen is tapped the speed of the pulse increases. Right now I have two issues. One, is that the animation does not occur, the second is that the animation must speed up without restarting the animation. I want the animation to speed up from whatever point it is at when the screen is tapped.
class ViewController: UIViewController {
var tapCount: Int = 0
var pulseSpeed: Double = 3
let pulseAnimLayer = CALayer()
let pulseAnim = CABasicAnimation(keyPath: "Opacity")
override func viewDidLoad() {
super.viewDidLoad()
counter.center = CGPoint(x: 185, y: 118)
pulseAnim.fromValue = 0.5
pulseAnim.toValue = 1.0
pulseAnim.duration = 3.0
pulseAnim.autoreverses = true
pulseAnim.repeatCount = .greatestFiniteMagnitude
pulseAnimLayer.add(pulseAnim, forKey: "Opacity")
}
func pulseAnimation(pulseSpeed: Double) {
UIView.animate(withDuration: pulseSpeed, delay: 0,
options: [UIViewAnimationOptions.repeat, UIViewAnimationOptions.autoreverse],
animations: {
self.red.alpha = 0.5
self.red.alpha = 1.0
}
)
}
#IBOutlet weak var red: UIImageView!
#IBOutlet weak var counter: UILabel!
#IBAction func screenTapButton(_ sender: UIButton) {
tapCount += 1
counter.text = "\(tapCount)"
pulseSpeed = Double(3) / Double(tapCount)
pulseAnim.duration = pulseSpeed
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}

I didn't read this carefully enough the first time.
You've got an odd mix here of core animation and UIView animation. The two exist at different levels. Core Animation will allow you to animate a property on a view (say it's background color).
UIView animation is built on top of Core Animation and makes it easier to animate view changes automatically by building the underlying Core Animations.
You start with Core Animation when you create a CABasicAnimation but then you have a function, pulseAnimation which looks to be doing something using UIVIew animations. I think you can get rid of that function entirely.
When you add your CABasicAnimation you tell it to change the value of the layer's "Opacity". What you probably want there is "opacity" (lower case).
Your animation is being applied to a new CALayer that you've created... but you don't seem to ever put that layer into the view (so it is never drawn). More likely than not what you should probably do is attach your animation to the view's layer and just allow that layer to have it's color animated.
To add one layer to another use addSublayer (https://developer.apple.com/reference/quartzcore/calayer/1410833-addsublayer). The view's layer from your code should be available as self.view.layer though you may have to use it in viewWillAppear instead of in viewDidLoad

You need to set the alpha of the red imageview to 0.0 before animating
func pulseAnimation(pulseSpeed: Double) {
UIView.animate(withDuration: pulseSpeed, delay: 1.0,
options: [UIViewAnimationOptions.repeat, UIViewAnimationOptions.autoreverse],
animations: {
self.red.alpha = 0.5
self.red.alpha = 1.0
}
)
}
override func viewDidAppear(_ animated: Bool) {
self.red.alpha = 0.0
counter.center = CGPoint(x: 185, y: 118)
}

Related

iOS - Transition between gradients?

I'm trying to rotate the background colour endlessly between two different gradients, but the actual transition isn't lasting as long as expected (it lasts for less than a second and there's no delay between each transition).
var backgroundColours = [CAGradientLayer()]
var backgroundLoop = 0
override func viewDidLoad() {
super.viewDidLoad()
backgroundColours = [Colors.init().one, Colors.init().two] // These are two CAGradientLayers but I haven't included their code
backgroundLoop = 0
self.animateBackgroundColour()
}
func animateBackgroundColour () {
backgroundLoop = (backgroundLoop + 1) % (backgroundColours.count)
UIView.animate(withDuration: 2.0, delay: 2.0, options: .curveLinear, animations: {
self.view.backgroundColor = UIColor.clear
let backgroundLayer = self.backgroundColours[self.backgroundLoop]
backgroundLayer.frame = self.view.frame
self.view.layer.insertSublayer(backgroundLayer, at: 0)
}, completion: { (Bool) -> Void in
self.animateBackgroundColour()
})
This is all inside the ViewController Class
UIView.animate animates only about five specialized view properties. You are not animating any of those properties. What you want is layer animation (e.g. CABasicAnimation).

Animate a rotated label in swift?

I am trying to animate a rotated label like this:
#IBOutlet fileprivate weak var loadingLabel: UILabel!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
loadingLabel.transform = CGAffineTransform(rotationAngle: CGFloat(0.2)) // rotation line
UIView.animate(withDuration: 2.0, animations: {
self.loadingLabel.transform = CGAffineTransform(translationX: 0, y: self.view.bounds.size.height)
})
}
When I comment out the rotation line of code (and keep the label unrotated), it works fine. But when I rotate it, the label starts off the screen at the beginning of the animation:
When I comment out the animation, the label is rotated perfectly fine (but no animation obviously):
How do I rotate the image and animate it, without having this weird placement?
Edit: To clarify: I want the label to start off rotated in the center of the screen, and just simply move the label. I do not want to rotate the image during the animation.
The correct answer is that you are supposed to concatenate the transformation matrices. If you don't want to do linear algebra then the easy way is that you use the transform to set the rotation and don't animate it, then animate the view's frame/center instead.
import UIKit
class V: UIViewController {
#IBOutlet var label: UILabel!
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
label.transform = CGAffineTransform(rotationAngle: CGFloat.pi / 6)
label.center.x += 300
UIView.animate(withDuration: 2) {
self.label.center.x -= 300
}
}
}
You can perform the animation with CABasicAnimation as it will give you more control on the animation and it has a completion block on which you can hide your label as well upon your requirement.
loadingLabel.transform = CGAffineTransform(rotationAngle: CGFloat(0.2)) // rotation line
let animationKey = "position.y"
CATransaction.begin()
let moveYAnimation = CABasicAnimation( keyPath: animationKey)
moveYAnimation.fromValue = loadingLabel.frame.origin.y
moveYAnimation.toValue = self.view.bounds.size.height
moveYAnimation.duration = 2
loadingLabel.layer.add( moveYAnimation, forKey: animationKey )
// Callback function
CATransaction.setCompletionBlock {
print("end animation")
self.loadingLabel.isHidden = true
}
// Do the actual animation and commit the transaction
loadingLabel.layer.add(moveYAnimation, forKey: animationKey)
CATransaction.commit()
Hope it will help you.
The first transform is ot of the animation block, it's why it begin out of the screen.
You should move it in the animation block, and use a completion handler to animate again.
UIView.animate(withDuration: 2.0, animations: {
//
}, completion: { (result) in
//
})
Be carefull, the angle is in radians.

Cannot tap UIButton after having set alpha to 0.0, even when reset to 1.0

I have this simple code:
func tappedButton() {
self.button.alpha = 1.0
UIView.animate(withDuration: 1.0, delay: 4.0, options: .curveLinear, animations: {
self.button.alpha = 0.0
}) { _ in }
}
This function aims at showing a button again for 4 seconds before hiding it (with a 1 second animation). However, while the button is completely visible for these 4 seconds, tapping it doesn't work anymore.
Thanks for your help.
As per the documentation in for the method hittest(_:with:) of UIView https://developer.apple.com/documentation/uikit/uiview/1622469-hittest
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit. Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content.
This means that any view, particularly a button, with alpha 0.0 would not be touched.
However, the problem here is that the button is still visible, at least for you. This odd behavior occurs because the actual alpha value of the button is already setted to 0.0 when the animations starts. Animations work by changing the visual hierachy and transition the difference with the parameters you give to the function. In your case, you have two states: a view with a visible button visible and another view without the button. Only the visual part is animated but the corresponding values are already setted. A solution would be:
func tappedButton() {
self.button.alpha = 1.0
UIView.animate(withDuration: 1.0, delay: 4.0, options: .curveLinear, animations: { [weak self] in
self?.button.alpha = 0.01
}) { [weak self] _ in self?.button.alpha = 0.0 }
}
EDIT: This solution seems like a hack but works. I use this approach because the completion handler is always called with a true value.
func tapped() {
let duration = 1.0
let delay = 2.0
let delayDuration = delay + duration
UIView.animate(withDuration: duration, delay: delay, options: [.curveLinear, .allowUserInteraction], animations: { [weak self] in
self?.saveButton.alpha = 0.1
})
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + delayDuration, execute: { [weak self] in
self?.saveButton.alpha = 0.0
})
}
You need to use allUserInteraction in the options and also check for touches. The animation is added immediately and although you see the button to the system it is already hidden. What does this mean? It means you are watching a movie. But at least with userInteraction enabled you can check for touch events. This is great but how do we know the button is really showing or not? Well you have to use two different checks most likely. One that checks the true UIView alpha of the button and one check that checks the opacity on the presentation layer. I have never fully looked at the link between UIView animations and Core Animation except that I think UIView animations are a wrapper for Core Animations. UIView animations definitely update the view model immediately. So an alpha animation is most likely interpreted into an opacity animation on the layer. Armed with this we can check the opacity of the presentation layer on touches and see that the button is being clicked even if the view model thinks the alpha is 0. This check on the presentation layer will work as long as the opacity is above 0. So here you go.
import UIKit
class ViewController: UIViewController {
lazy var testButton : UIButton = {
let v = UIButton(frame: CGRect(x: 20, y: 50, width: self.view.bounds.width - 40, height: 50))
v.backgroundColor = UIColor.blue
v.addTarget(self, action: #selector(buttonClicked), for: .touchUpInside)
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(testButton)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 1.0, delay: 4.0, options: .allowUserInteraction, animations: {
self.testButton.alpha = 0
}, completion: nil)
//test the layer
//test the layer for opacity
if let presentation = testButton.layer.presentation()?.animation(forKey: "opacity"){
print("the animation is already added so normal clicks won't work")
}
}
#objc func buttonClicked(){
print("clicked")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touch = touches.first{
let location = touch.location(in: self.view)
if self.testButton.frame.contains(location){
//but what you might not know is the animation is probably already running
//and so the check above misses this
if let buttonPres = testButton.layer.presentation(),
let _ = buttonPres.animation(forKey: "opacity"),
let opacity = buttonPres.value(forKey: "opacity") as? CGFloat{
if opacity > 0{
buttonClicked()
}
}
}
}
}
}

How to animate a custom property in iOS

I have a custom UIView that draws its contents using Core Graphics calls. All working well, but now I want to animate a change in value that affects the display. I have a custom property to achieve this in my custom UView:
var _anime: CGFloat = 0
var anime: CGFloat {
set {
_anime = newValue
for(gauge) in gauges {
gauge.animate(newValue)
}
setNeedsDisplay()
}
get {
return _anime
}
}
And I have started an animation from the ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.emaxView.anime = 0.5
UIView.animate(withDuration: 4) {
DDLogDebug("in animations")
self.emaxView.anime = 1.0
}
}
This doesn't work - the animated value does change from 0.5 to 1.0 but it does so instantly. There are two calls to the anime setter, once with value 0.5 then immediately a call with 1.0. If I change the property I'm animating to a standard UIView property, e.g. alpha, it works correctly.
I'm coming from an Android background, so this whole iOS animation framework looks suspiciously like black magic to me. Is there any way of animating a property other than predefined UIView properties?
Below is what the animated view is supposed to look like - it gets a new value about every 1/2 second and I want the pointer to move smoothly over that time from the previous value to the next. The code to update it is:
open func animate(_ progress: CGFloat) {
//DDLogDebug("in animate: progress \(progress)")
if(dataValid) {
currentValue = targetValue * progress + initialValue * (1 - progress)
}
}
And calling draw() after it's updated will make it redraw with the new pointer position, interpolating between initialValue and targetValue
Short answer: use CADisplayLink to get called every n frames. Sample code:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let displayLink = CADisplayLink(target: self, selector: #selector(animationDidUpdate))
displayLink.preferredFramesPerSecond = 50
displayLink.add(to: .main, forMode: .defaultRunLoopMode)
updateValues()
}
var animationComplete = false
var lastUpdateTime = CACurrentMediaTime()
func updateValues() {
self.emaxView.animate(0);
lastUpdateTime = CACurrentMediaTime()
animationComplete = false
}
func animationDidUpdate(displayLink: CADisplayLink) {
if(!animationComplete) {
let now = CACurrentMediaTime()
let interval = (CACurrentMediaTime() - lastUpdateTime)/animationDuration
self.emaxView.animate(min(CGFloat(interval), 1))
animationComplete = interval >= 1.0
}
}
}
The code could be refined and generalised but it's doing the job I needed.
You will need to call layoufIfNeeded() instead of setNeedsDisplay() if you modify any auto layout constraints in your gauge.animate(newValue) function.
https://stackoverflow.com/a/12664093/255549
If that is drawn entirely with CoreGraphics there is a pretty simple way to animate this if you want to do a little math. Fortunately you have a scale there that tells you the number of radians exactly to rotate, so the math is minimal and no trigonometry is involved. The advantage of this is you won't have to redraw the entire background, or even the pointer. It can be a bit tricky to get angles and stuff right, I can help out if the following doesn't work.
Draw the background of the view normally in draw(in rect). The pointer you should put into a CALayer. You can pretty much just move the draw code for the pointer, including the centre dark gray circle into a separate method that returns a UIImage. The layer will be sized to the frame of the view (in layout subviews), and the anchor point has to be set to (0.5, 0.5), which is actually the default so you should be ok leaving that line out. Then your animate method just changes the layer's transform to rotate according to what you need. Here's how I would do it. I'm going to change the method and variable names because anime and animate were just a bit too obscure.
Because layer properties implicitly animate with a duration of 0.25 you might be able to get away without even calling an animation method. It's been a while since I've worked with CoreAnimation, so test it out obviously.
The advantage here is that you just set the RPM of the dial to what you want, and it will rotate over to that speed. And no one will read your code and be like WTF is _anime! :) I have included the init methods to remind you to change the contents scale of the layer (or it renders in low quality), obviously you may have other things in your init.
class SpeedDial: UIView {
var pointer: CALayer!
var pointerView: UIView!
var rpm: CGFloat = 0 {
didSet {
pointer.setAffineTransform(rpm == 0 ? .identity : CGAffineTransform(rotationAngle: rpm/25 * .pi))
}
}
override init(frame: CGRect) {
super.init(frame: frame)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
pointer = CALayer()
pointer.contentsScale = UIScreen.main.scale
pointerView = UIView()
addSubview(pointerView)
pointerView.layer.addSublayer(pointer)
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
context.saveGState()
//draw background with values
//but not the pointer or centre circle
context.restoreGState()
}
override func layoutSubviews() {
super.layoutSubviews()
pointerView.frame = bounds
pointer.frame = bounds
pointer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pointer.contents = drawPointer(in: bounds)?.cgImage
}
func drawPointer(in rect: CGRect) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
guard let context = UIGraphicsGetCurrentContext() else { return nil }
context.saveGState()
// draw the pointer Image. Make sure to draw it pointing at zero. ie at 8 o'clock
// I'm not sure what your drawing code looks like, but if the pointer is pointing
// vertically(at 12 o'clock), you can get it pointing at zero by rotating the actual draw context like so:
// perform this context rotation before actually drawing the pointer
context.translateBy(x: rect.width/2, y: rect.height/2)
context.rotate(by: -17.5/25 * .pi) // the angle judging by the dial - remember .pi is 180 degrees
context.translateBy(x: -rect.width/2, y: -rect.height/2)
context.restoreGState()
let pointerImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return pointerImage
}
}
The pointer's identity transform has it pointing at 0 RPM, so every time you up the RPM to what you want, it will rotate up to that value.
edit: tested it, it works. Except I made a couple errors - you don't need to change the layers position, I updated the code accordingly. Also, changing the layer's transform triggers layoutSubviews in the immediate parent. I forgot about this. The easiest way around this is to put the pointer layer into a UIView that is a subview of SpeedDial. I've updated the code. Good luck! Maybe this is overkill, but its a bit more reusable than animating the entire rendering of the view, background and all.

Swift UIView appearing in different places

Im trying to make a small game. And there is some problem with the animation. Im new to Swift. So lets take a look. I create a UIImageView picture and want to do animation of this picture appear in a different places on a screen. I believe that the algorithm will look like this:
Infinite loop{
1-GetRandomPlace
2-change opacity from 0 to 1 and back(with smooth transition)
}
Looks simple, but I can't understand how to do it correctly in Xcode.
Here is my test code but it looks useless
Thank you for help and sorry if there was already this question, I can't find it.
class ViewController: UIViewController {
#IBOutlet weak var BackgroundMainMenu:UIImageView!
#IBOutlet weak var AnimationinMenu: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
// MoveBackgroundObject(AnimationinMenu)
// AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
// self.AnimationinMenu.alpha = 0
//
// UIImageView.animateWithDuration(3.0,
// delay: 0.0,
// options: UIViewAnimationOptions([.Repeat, .CurveEaseInOut]),
// animations: {
// self.MoveBackgroundObject(self.AnimationinMenu)
// self.AnimationinMenu.alpha = 1
// self.AnimationinMenu.alpha = 0
// },
// completion: nil)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewWillAppear(animated: Bool) {
AnimationBackgroundDots(AnimationinMenu, delay: 0.0)
}
func ChangeOpacityto1(element: UIImageView){
element.alpha = 0
UIView.animateWithDuration(3.0) {
element.alpha = 1
}
}
func ChangeOpacityto0(element: UIImageView){
element.alpha = 1
UIView.animateWithDuration(3.0){
element.alpha = 0
}
}
func AnimationBackgroundDots(element: UIImageView, delay: Double){
element.alpha = 0
var z = 0
while (z<4){
MoveBackgroundObject(AnimationinMenu)
UIImageView.animateWithDuration(3.0,
animations: {
element.alpha = 0
element.alpha = 1
element.alpha = 0
},
completion: nil)
z++
}
}
func MoveBackgroundObject(element: UIImageView) {
// Find the button's width and height
let elementWidth = element.frame.width
let elementHeight = element.frame.height
// Find the width and height of the enclosing view
let viewWidth = BackgroundMainMenu.superview!.bounds.width
let viewHeight = BackgroundMainMenu.superview!.bounds.height
// Compute width and height of the area to contain the button's center
let xwidth = viewWidth - elementWidth
let yheight = viewHeight - elementHeight
// Generate a random x and y offset
let xoffset = CGFloat(arc4random_uniform(UInt32(xwidth)))
let yoffset = CGFloat(arc4random_uniform(UInt32(yheight)))
// Offset the button's center by the random offsets.
element.center.x = xoffset + elementWidth / 2
element.center.y = yoffset + elementHeight / 2
}
}
The problem is in AnimationBackgroundDots.
You are immediately creating 4 animations on the same view but only one can run at a time. What you need to do is wait until one animation is finished (fade in or fade out) before starting a new one.
Also, the animations closure is for setting the state you want your view to animate to. It looks at how your view is at the start, runs animations, then looks at the view again and figures out how to animate between the two. In your case, the alpha of the UIImageView starts at 0, then when animations runs, the alpha ends up being 0 so nothing would animate. You can't create all the steps an animation should take that way.
Want you need to do it move your view and start the fade in animation. The completion closure of fading in should start the fade out animation. The completion closure of the fading out should then start the process all over again. It could look something like this.
func AnimationBackgroundDots(element: UIImageView, times: Int) {
guard times > 0 else {
return
}
MoveBackgroundObject(element)
element.alpha = 1
// Fade in
UIView.animateWithDuration(3.0, animations: {
element.alpha = 1
}, completion: { finished in
// Fade Out
UIView.animateWithDuration(3.0, animations: {
element.alpha = 0
}, completion: { finished in
// Start over again
self.AnimationBackgroundDots(element, times: times-1)
})
})
}
You called also look at using keyframe animations but this case is simple enough that theres no benefit using them.
Also as a side note. The function naming convention in swift is to start with a lowercase letter, so AnimationBackgroundDots should be animationBackgroundDots.

Resources