I’m working on a project that has about 16 UIViews on a ViewController.
Some of the UIView contain some processor intensive CG drawings with transparency done at load time, while other UIView are animating. In particular, the following rotating animations run continuously. (If I look very closely, the animations appear not entirely smooth.)
The device I’m testing on is warming up, it’s not hot, but getting very warm. I’m concerned I’m overloading the processor.
Question:
Is there a more efficient way to complete the below animations which run continuously using a method that is less resource hungry and prevents the device from heating up?
Xcode instruments:
At load (max): CPU: 56% - Memory: 29MB
When running (max): CPU: 1% - Memory: 29MB
func rotateForward() {
UIView.animateWithDuration(2, delay: 0, options: .CurveLinear, animations: { () -> Void in
self.viewA.transform = CGAffineTransformRotate(self.viewA.transform, CGFloat(M_PI_2))
self.viewB.transform = CGAffineTransformRotate(self.viewB.transform, CGFloat(M_PI_2))
self.viewC.transform = CGAffineTransformRotate(self.viewC.transform, CGFloat(M_PI_2))
self.viewD.transform = CGAffineTransformRotate(self.viewD.transform, CGFloat(M_PI_2))
}) { (finished) -> Void in
self.rotateForward()
}
}
func rotateBack() {
UIView.animateWithDuration(2, delay: 0, options: .CurveLinear, animations: { () -> Void in
self.viewW.transform = CGAffineTransformRotate(self.viewW.transform, -CGFloat(M_PI_2))
self.viewX.transform = CGAffineTransformRotate(self.viewX.transform, -CGFloat(M_PI_2))
self.viewY.transform = CGAffineTransformRotate(self.viewY.transform, -CGFloat(M_PI_2))
self.viewZ.transform = CGAffineTransformRotate(self.viewZ.transform, -CGFloat(M_PI_2))
}) { (finished) -> Void in
self.rotateBack()
}
}
Instead of using CPU to run these animation , you can use GPU to perform them.
You can use this protocol to make the view rotate forward. Try to subclass the views to implement this protocol and try it out.
protocol Rotate{ }
extension Rotate where Self: UIView {
func rotate() {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.duration = 2
animation.repeatCount = Float.infinity
animation.fromValue = 0.0
animation.toValue = Float(M_PI * 2.0)
layer.addAnimation(animation, forKey: "position")
}
}
Related
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).
There are so many posts similar to this that I have seen and none of them works.
Here is my code so far.
func startRotating(view : UIImageView, rotating : Bool) {
//Rotating the rotatingClockBarImage
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveLinear], animations: {
view.transform = CGAffineTransform(rotationAngle: CGFloat.pi)
}, completion: {finished in
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveLinear], animations: {
view.transform = CGAffineTransform(rotationAngle: 0)
}, completion: nil)
})//End of rotation
continueRotating(view: view)
}
The original problem would be that I couldn't rotate a full 360 degrees. I figured that out by rotating half way and the other half in the completion.
The problem now is once this animation finishes, that's it. I have tried putting it in a while loop, for loop, calling two similar functions back and forth. Nothing works it just keeps freezing my app.
In a for loop, for example, that would run 3 times, I put a print(). The print writes to the console three times but the animation only happens once. Because of this I am thinking the animation is just cutting itself off before it even starts, and the final rotation is the only one that plays through. So I need to find a way to allow it to play each rotation through.
This shouldn't be that hard seeing that Apple had their planes rotate so easily in a former version of Xcode in a game app. I'm trying to avoid deleting and reinstalling the old version just so I can look at that code.
Actually it would be more easy:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 1.0) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
func stopRotating(){
self.layer.sublayers?.removeAll()
//or
self.layer.removeAllAnimations()
}
}
Then for rotating:
yourView.rotate360Degrees()
for stopping:
yourView. stopRotating()
Did you try calling startRotating again in the completion block of your second half turn ?
Note that you should do that conditionally with a "stop" flag of your own if you ver want it to stop.
To remove a subView from the main view (the subView "dims" the parent view, hence why it's called dimView) I use an animation; it basically moves the subView toward the bottom of the screen and eventually out of the screen:
let centerY = CGRectGetMidY(view.bounds)
let animation: CABasicAnimation = CABasicAnimation(keyPath: "position")
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
animation.fromValue = NSValue(CGPoint: view.center)
animation.toValue = NSValue(CGPoint: CGPoint(x: view.center.x, y: 4 * centerY))
animation.duration = 0.2
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
dimView.layer.addAnimation(animation, forKey: "DimRemovingAnimation")
Then through an event driven function I call this animation like so:
#IBAction func done(sender: AnyObject) {
let dimView = view.viewWithTag(1)
if let dimView = dimView {
removingAnimation(dimView)
Delay.delay(0.4){
dimView.removeFromSuperview()
}
}
}
This is my Delay class:
class Delay{
class func delay(delay: Double, block: () -> ()){
let when = dispatch_time(DISPATCH_TIME_NOW, Int64(Int(delay) * Int(NSEC_PER_SEC)))
dispatch_after(when, dispatch_get_main_queue(), block)
}
}
When I run this closure on the main thread (which is a serial thread, and should execute tasks in a serial manner) my animation is ignored and dimView is removed from the view hierarchy at once. However, when I run the closure on a global concurrent thread the animation is not ignored and the code successfully removes dimView of the hierarchy. But this is illegal since you must access UIKit from the main thread.
Can you please explain to me what the problem is when invoking the closure on the main thread? and a possible solution to my problem?
Thanks
The problem is that you're converting delay of 0.4 to an Int, which is 0. I'd suggest replacing the reference to
Int64(Int(delay) * Int(NSEC_PER_SEC))
with
Int64(delay * Double(NSEC_PER_SEC))
As a further refinement, though, rather than triggering the removal after a certain amount of time has passed, I'd let the animation tell you when it's done. Easiest, you can use the block base UIView animation:
UIView.animateWithDuration(0.2, delay: 0.0, options: .CurveEaseIn, animations: {
dimView.center = CGPoint(x: self.view.center.x, y: 4 * centerY)
}, completion: { finished in
dimView.removeFromSuperview()
})
Or, if you must use CABasicAnimation, specify its delegate and then implement animationDidStop delegate method to remove the view:
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
dimView.removeFromSuperview()
}
By the way, all of the above assumes you have not specified auto layout constraints for the dimView. If you have, rather than animating by changing the frame-related properties, you should modify those constraints and then animate the call to layoutIfNeeded().
When doing UIView.animateWithDuration, I would like to define a custom curve for the ease, as opposed to the default: .CurveEaseInOut, .CurveEaseIn, .CurveEaseOut, .CurveLinear.
This is an example ease that I want applied to UIView.animateWithDuration:
let ease = CAMediaTimingFunction(controlPoints: Float(0.8), Float(0.0), Float(0.2), Float(1.0))
I tried making my own UIViewAnimationCurve, but it seems it accepts only one Int.
I can apply the custom ease to a Core Animation, but I would like to have custom easing for UIView.animateWithDuration for simpler and optimized code. UIView.animateWithDuration is better for me as I won't have to define animations for each animated property and easier completion handlers, and to have all animation code in one function.
Here's my non-working code so far:
let customOptions = UIViewAnimationOptions(UInt((0 as NSNumber).integerValue << 50))
UIView.setAnimationCurve(UIViewAnimationCurve(rawValue: 5)!)
UIView.animateWithDuration(2, delay: 0, options: customOptions, animations: {
view.layer.position = toPosition
view.layer.bounds.size = toSize
}, completion: { finished in
println("completion")
})
That's because UIViewAnimationCurve is an enumeration - its basically human-readable representations of integer values used to determine what curve to use.
If you want to define your own curve, you need to use CA animations.
You can still do completion blocks and groups of animations. You can group multiple CA Animations into a CAAnimationGroup
let theAnimations = CAAnimationGroup()
theAnimations.animations = [positionAnimation, someOtherAnimation]
For completion, use a CATransaction.
CATransaction.begin()
CATransaction.setCompletionBlock { () -> Void in
// something?
}
// your animations go here
CATransaction.commit()
After a bunch of researches, the following code works for me.
Create an explicit core animation transaction, set your desired timing function.
Use eigher or both UIKit and CAAnimation to perform the changes.
let duration = 2.0
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(controlPoints: 0.8, 0.0, 0.2, 1.0))
CATransaction.setCompletionBlock {
print("animation finished")
}
// View animation
UIView.animate(withDuration: duration) {
// E.g. view.center = toPosition
}
// Layer animation
view.layer.position = toPosition
view.layer.bounds.size = toSize
CATransaction.commit()
With iOS 10.0 you can use UIViewPropertyAnimator.
https://developer.apple.com/documentation/uikit/uiviewpropertyanimator
See https://developer.apple.com/videos/play/wwdc2016/216/
After playing around a lot with the UIView dynamic animations introduced in iOS 7, most notably:
[UIView animateWithDuration: delay: usingSpringWithDamping: initialSpringVelocity: options: animations: completion:];
I was wondering if there is an equivalent to 'SpringWithDamping/Velocity' method that can be accessed directly when creating a CALayer animation? I.e. either through CATransaction, CABasicAnimation or otherwise...
Thanks
in iOS9 Apple finally made the CASpringAnimation class public.
You can use it like that:
let spring = CASpringAnimation(keyPath: "position.x")
spring.damping = 5
spring.fromValue = myLayer.position.x
spring.toValue = myLayer.position.x + 100.0
spring.duration = spring.settlingDuration
myLayer.addAnimation(spring, forKey: nil)
Notice that you cannot set the animation duration - you need to ask the CASpringAnimation class for the settlingDuration (e.g. "How much time is going to take for the spring system to settle down") and then set it as the duration of your animation.
Check the header files for CASpringAnimation - it exposes a number of spring system variables you can adjust - stiffness, mass, etc.
There is (and have been for a while) a private class called CASpringAnimation that I'm pretty sure is being used behind it all (but I haven't verified it). Unfortunately, it is still private.
As David said, CASpringAnimation is private (for now?), but I recently came across RBBSpringAnimation from the RBBAnimation project.
I can definitely recommend this, it was very easy to drop in as a replacement for my existing CABasicAnimation.
I wrote a class to create CASpringAnimation instance. It works in a pretty simple way:
By creating a spring animation from UIKit API, it arrests the created CASpringAnimation instance from the view's layer, copies it and returns it.
But I don't know if it is App Store safe that to create CASpringAnimation in this way.
import UIKit
private let SharedCASpringAnimationFactory = CASpringAnimationFactory()
public class CASpringAnimationFactory {
private var dummyView: UIView
private init() {
dummyView = UIView(frame: CGRect.zeroRect)
}
private class var shared: CASpringAnimationFactory {
return SharedCASpringAnimationFactory
}
public class func animation(#keyPath: String, dumping: CGFloat, initialSpringVelocity: CGFloat) -> CABasicAnimation {
let duration = CATransaction.animationDuration()
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: dumping, initialSpringVelocity: initialSpringVelocity, options: nil,
animations: { () -> Void in
CASpringAnimationFactory.shared.dummyView.bounds = CGRect(origin: CGPoint.zero, size: CGSize(width: 100, height: 100))
}, completion: nil)
let dummyLayer = CASpringAnimationFactory.shared.dummyView.layer
let animations = dummyLayer.animationKeys().map {dummyLayer.animationForKey($0 as String) as CAAnimation}
let arrestedAnimation = animations.first!.copy() as CABasicAnimation
arrestedAnimation.keyPath = keyPath
arrestedAnimation.fromValue = nil
arrestedAnimation.toValue = nil
dummyLayer.removeAllAnimations()
shared.dummyView.bounds = CGRect.zeroRect
return arrestedAnimation
}
}