In my application I have several fades. I create them with Core Animation, using syntax similar to what is below. Is there a less verbose syntax I could use?
CATransaction.begin()
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 0
fade.toValue = 1
fade.duration = 0.35
fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
fade.fillMode = kCAFillModeForwards
fade.removedOnCompletion = false
CATransaction.setCompletionBlock({
...
})
self.addAnimation(fade, forKey: nil)
CATransaction.commit()
Sure. You can make your own:
extension CABasicAnimation
{
convenience init(_ keyPath: String, from: AnyObject?, to: AnyObject?, duration: CFTimeInterval = 0.3)
{
self.init(keyPath: keyPath)
fromValue = from
toValue = to
self.duration = duration
}
}
extension CALayer
{
func addAnimation(animation: CAAnimation, forKey key: String?, completion: Void -> Void)
{
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
addAnimation(animation, forKey: key)
CATransaction.commit()
}
}
// later...
let fade = CABasicAnimation("opacity", from: 0, to: 1, duration: 0.35)
self.addAnimation(fade, forKey: nil) {
// ...
}
As an alternative to #jtbandes' excellent answer (voted), in many cases you can use a much simpler UIView animation:
UIView.animateWidhDuration(.35
animations:
{
myView.alpha = 0
}
completion:
{
(finshed) in
//your completion code here
})
EDIT:
I see from your comment that you're developing for OS X. That requires a different technique, using the animation proxy for your view:
myView.animator.setAlphaValue(0.0)
And if you want it to use a different duration than the default 0.25 second duration, you need to set that in an animation context group:
NSAnimationContext.currentContext().beginGrouping()
NSAnimationContext.currentContext().setDuration(0.35)
myView.animator.setAlphaValue(0.0)
NSAnimationContext.currentContext().endGrouping()
I believe that unlike iOS, NSViews don't have a native alpha setting, and setAlphaValue actually manipulates the alpha value of the view's layer. In that case you will probably need to set the view as layer-backed.
Related
I am building my first game with SpriteKit, written in Swift 3.
I have been trying for hours to accomplish this "winning stars effect", but without any acceptable result yet :/
The effect i am looking for is demonstrated in this video: https://youtu.be/9CeK5_G8T9E
It's not a problem adding one or multiple stars; the problem is to make them move in different directions by different paths to the same location, just like it's done in this video example.
Hope for your help to lead me in the right direction.
Any solution, tutorials, examples etc. is more than welcome.
Thanks for your time.
Took a while but here's something:
import SpriteKit
class GameScene: SKScene {
var sprite = SKSpriteNode()
override func didMove(to view: SKView) {
for _ in 1...15 {
spawnsprite()
}
}
func spawnsprite() {
sprite = SKSpriteNode(color: SKColor.yellow, size: CGSize(width: 50, height: 50))
sprite.position = CGPoint(x: (-size.width/2)+50, y: (-size.height/2)+50)
addChild(sprite)
let destination = CGPoint(x: (size.width/2)-50, y: (size.height/2)-50)
let path = createCurvedPath(from: sprite.position, to: destination, varyingBy: 500)
let squareSpeed = CGFloat(arc4random_uniform(600)) + 200
let moveAction = SKAction.follow(path, asOffset: false, orientToPath: false, speed: squareSpeed)
let rotateAction = SKAction.repeatForever(SKAction.rotate(byAngle: 2 * CGFloat.pi, duration: 2))
sprite.run(SKAction.group([moveAction, rotateAction]))
}
func createCurvedPath(from start: CGPoint, to destination: CGPoint, varyingBy offset: UInt32) -> CGMutablePath {
let pathToMove = CGMutablePath()
pathToMove.move(to: start)
let controlPoint = CGPoint(x: CGFloat(arc4random_uniform(offset)) - CGFloat(offset/2),
y: CGFloat(arc4random_uniform(offset)) - CGFloat(offset/2))
pathToMove.addQuadCurve(to: destination, control: controlPoint)
return pathToMove
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
There's a lot that can be done to make this nicer (e.g. subclass SKSpriteNode and make the path that the ode follows a property of the sprite itself), but it illustrates the principle.
It could probably also be done with an SKEmitter.
Looking through Apple's documentation for UIImageOrientation I notice that the images that go with the descriptions are incorrect.
https://developer.apple.com/reference/uikit/uiimageorientation
This has been painful for me, so I'm going to leave this here, with the correct images in the answer in case others find the same.
If people think this shouldn't be here, please comment / vote down and I'll remove.
Here's how I got the correct images:
extension UIImage {
var normalised: UIImage {
if imageOrientation == .up {
return self
}
var normalisedImage: UIImage
let format = UIGraphicsImageRendererFormat.default()
format.scale = scale
format.opaque = true
format.prefersExtendedRange = false
normalisedImage = UIGraphicsImageRenderer(size: size, format: format).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
return normalisedImage
}
func translated(to orientation: UIImageOrientation) -> UIImage {
guard let cgImage = cgImage else {
return self
}
return UIImage(cgImage: cgImage, scale: 1, orientation: orientation).normalised
}
}
Then using this image as the "base"
I do widget. I need to changed cell height with animation. When click "show more" go extra controls.
enter image description here
I wrote this code
tableView.beginUpdates()
showModes(alpha: 1, duration: 0.2, delay: 0.1)
tableView.endUpdates()
func showModes(alpha: CGFloat, duration: CGFloat, delay: CGFloat) {
if tableView.numberOfRows(inSection: 0) != 0 {
for i in 0...tableView.numberOfRows(inSection: 0)-1 {
let indexPath = IndexPath(row: i, section: 0)
if let cell = tableView.cellForRow(at: indexPath) as? WidgetCell {
UIView.animate(withDuration: TimeInterval(duration), delay: TimeInterval(delay) ,animations: {
if alpha == 0 {
cell.favoriteConstraint.constant = -45
} else {
cell.favoriteConstraint.constant = 10
}
cell.favoriteMode.alpha = alpha
self.tableView.layoutIfNeeded()
})
}
}
}
}
But it does not work smoothly. it dramatically twitches up and down. How do I get this to work smoothly, for example, in the weather widget by Apple?
I can't recognise what is a problem definetly because you posted only part of your code. But I can suggest several checks to do
Did you set self-sizing cells for UITableView? AtableView.rowHeight = UITableViewAutomaticDimension;
Did you added constraint (favoriteConstraint) to cell's contentView
Also when animating constraint changes you don't need to change it inside animation closure.
The standard code looks like
cell.favoriteConstraint.constant = newValue
UIView.animateWithDuration(0.3) {
cell.contentView.layoutIfNeeded()
}
Also you don't need to wrap your method call. This must be ok:
showModes(alpha: 1, duration: 0.2, delay: 0.1)
tableView.beginUpdates()
tableView.endUpdates()
How can I make a effect in swift similar to this:
I want the animation to loop forever.
For iOS
UIViewAnimationOptions set provides different handy options to achieve a combination of beautiful and complex animations. For your particular scenario you will require two of the options.
UIViewAnimationOptions.Repeat
UIViewAnimationOptions.AutoReverse
Check out the code below for implementation.
Code:
class MyViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let view = UIView(frame: CGRect(x: 0, y: 0, width: 200, height: 200))
view.backgroundColor = UIColor.blueColor()
self.view.addSubview(view)
UIView.animateWithDuration(1,
delay: 0,
options: [UIViewAnimationOptions.Autoreverse, UIViewAnimationOptions.Repeat],
animations: {
view.backgroundColor = UIColor.clearColor()
},
completion: nil)
}
}
Explanation:
I have created a view with a specific frame for demo purpose.
The part you are interested in is the UIView.animateWithDuration method. Notice that I have provided an array [UIViewAnimationOptions.AutoReverse, UIViewAnimationOptions.Repeat] in the options parameter.
These two options are enough to achieve a smooth and forever looping animation like below.
https://s3.amazonaws.com/uploads.hipchat.com/37040/1764070/6Iow7n7WiWf6Naz/autoReverse.gif
If you don't want to reverse the animation, just remove UIViewAnimationOptions.AutoReverse from the array in the options parameter. You will get an animation like this.
https://s3.amazonaws.com/uploads.hipchat.com/37040/1764070/8fyRUlzqNHSQI47/noreverse.gif
For iOS
let viewSquare be the name of the blue square in your question.
UIView.animate(withDuration: 0.5, delay: 0, options: [.repeat,.autoreverse], animations: {
viewSquare.alpha = 0.0
}, completion: nil)
Swift 5.1
let duration = 0.5
func fadeIn(finished: Bool) {
UIView.animate(withDuration: self.duration, delay: 0,
options: [.curveEaseInOut],
animations: { self.tftMap.alpha = 1 }, completion: self.fadeOut)
}
func fadeOut(finished: Bool) {
UIView.animate(withDuration: self.duration, delay: 0,
options: [.curveEaseInOut],
animations: { self.tftMap.alpha = 0 }, completion: self.fadeIn)
}
I assume you are programming for iOS.
Play around with the duration to see what suits you best:
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
let duration = 0.5
override func viewDidLoad() {
super.viewDidLoad()
self.fadeOut(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func fadeIn(finished: Bool) {
UIView.animateWithDuration(self.duration, delay: 0, options: [.CurveEaseInOut], animations: { self.myView.alpha = 1 } , completion: self.fadeOut)
}
func fadeOut(finished: Bool) {
UIView.animateWithDuration(self.duration, delay: 0, options: [.CurveEaseInOut], animations: { self.myView.alpha = 0 } , completion: self.fadeIn)
}
}
Swift 5
This worked well for me. I wanted to animate a continuous fade in and out of a label, which I placed inside the "cardHeaderView" UIView.
#IBOutlet weak var cardHeaderView: UIView!
Place this inside the "viewDidAppear". I went with a delay of zero so the animation would start right away.
fadeViewInThenOut(view: cardHeaderView, delay: 0)
Here is the function.
func fadeViewInThenOut(view : UIView, delay: TimeInterval) {
let animationDuration = 1.5
UIView.animate(withDuration: animationDuration, delay: delay, options: [UIView.AnimationOptions.autoreverse, UIView.AnimationOptions.repeat], animations: {
view.alpha = 0
}, completion: nil)
}
Swift 5
Fade in 2 seconds
pause 2 seconds,
fade out 2 seconds
Repeat
func startAnimation() {
UIView.animateKeyframes(withDuration: 6.0,
delay: 0,
options: [.repeat, .autoreverse, .calculationModeLinear]) {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.165) { [weak self] in
self?.view1.alpha = 0.0
}
UIView.addKeyframe(withRelativeStartTime: 0.165, relativeDuration: 0.165) { [weak self] in
self?.view2.alpha = 1.0
}
UIView.addKeyframe(withRelativeStartTime: 0.66, relativeDuration: 0.165) { [weak self] in
self?.view2.alpha = 0.0
}
ctartTime: 0.825, relativeDuration: 0.165) { [weak self] in
self?.view1.alpha = 1.0
}
}
}
Please note that when your animation is at Alpha == 0.0 the item is not interactable! You will have to add to .allowUserInteraction as an option
If you want repeatable fade animation you can do that by using CABasicAnimation like below :
###First create handy UIView extension :
extension UIView {
enum AnimationKeyPath: String {
case opacity = "opacity"
}
func flash(animation: AnimationKeyPath ,withDuration duration: TimeInterval = 0.5, repeatCount: Float = 5){
let flash = CABasicAnimation(keyPath: AnimationKeyPath.opacity.rawValue)
flash.duration = duration
flash.fromValue = 1 // alpha
flash.toValue = 0 // alpha
flash.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
flash.autoreverses = true
flash.repeatCount = repeatCount
layer.add(flash, forKey: nil)
}
}
###How to use it:
// You can use it with all kind of UIViews e.g. UIButton, UILabel, UIImage, UIImageView, ...
imageView.flash(animation: .opacity, withDuration: 1, repeatCount: 5)
titleLabel.flash(animation: .opacity, withDuration: 1, repeatCount: 5)
For some reason after my first call of a serious of animation blocks, the animation seems to be faster, not sure if this is a bug or something i've done wrong but i'm sure someone call tell me.. i've made a UIView subclass to handle this.
import UIKit
import QuartzCore
class GBPopupController: UIView {
var originalContainerCenterY = CGFloat()
#IBOutlet var continerConstraintCenterY: NSLayoutConstraint!
#IBOutlet var containerConstraintCenterX: NSLayoutConstraint!
var startingCenter = CGPoint()
#IBOutlet var contentView: UIView!
#IBOutlet var button: UIButton!
override func layoutSubviews() {
super.layoutSubviews()
contentView.layer.cornerRadius = 10
button.layer.cornerRadius = 10
}
override func didMoveToSuperview() {
self.beginViewAnimations()
}
func animatePopupIn() {
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0.7, options: UIViewAnimationOptions.CurveEaseIn, animations: {
self.contentView.layer.transform = CATransform3DIdentity
}, completion: {finished in
})
}
func beginViewAnimations() {
var transform = CATransform3DIdentity;
transform = CATransform3DMakeTranslation(0, -self.frame.size.height, 0)
self.contentView.layer.transform = transform
UIView.animateWithDuration(0.4, delay: 0.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
self.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
}, completion: {finished in
self.animatePopupIn()
})
}
func removeViewFromSuperView() {
UIView.animateWithDuration(0.5, delay: 0.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
var transform = CATransform3DIdentity;
transform = CATransform3DMakeTranslation(0, -self.frame.size.height, 0)
self.contentView.layer.transform = transform
}, completion: {finished in
UIView.animateWithDuration(0.4, delay: 0.0, options: UIViewAnimationOptions.TransitionCrossDissolve, animations: {
self.backgroundColor = UIColor.clearColor()
}, completion: {finished in
self.removeViewAnimation()
})
})
}
func removeViewAnimation() {
self.removeFromSuperview()
}
My comment is too long so I answer here. I don't know if it can help you but here's what I did.
I use autolayout and so in the viewdidload(), dimensions are not yet initialized. They are based on a 600x600 size screen (meaning any width, any height). Screen size is picked up once the first action is done (like pushing a button in my case).
This is why I call my animation for the first time at this first action, just to put my frame at the good place. Then the rest is the same. But it kind of initialize my view with the right frame (size and position). After that, I don't have this problem and my animation is also good.
It's like I was calling it twice the first time, one to initialize and one just to play the animation.
I made some research and I found that it could be a problem with the layer which is different from the frame and which is initialized with a different animation from the one you choose. That's why I didn't have the same animation the first time.
Hope it will help you.