I want to transform my image view like this:
I use landscape mode, I want the panda begin at the center vertically from the left, then it will rotate 360 again and again with become bigger and bigger, and go to the center vertically to the right.
I have tried like this:
import UIKit
class ViewController: UIViewController {
#IBOutlet var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let scale = CGAffineTransformMakeScale(3.0, 3.0)
let rotate = CGAffineTransformMakeRotation(180*CGFloat(M_PI)/180)
let translate = CGAffineTransformMakeTranslation(UIScreen.mainScreen().bounds.width - 100, 0)
UIView.animateWithDuration(3.0, delay: 0, options: [.Repeat, .Autoreverse, .CurveEaseInOut], animations: { () -> Void in
let mixTransform = CGAffineTransformConcat(scale, translate)
self.imageView.transform = mixTransform
self.view.layoutIfNeeded()
}, completion: nil)
UIView.animateWithDuration(1.0, delay: 0, options: [.Repeat, .Autoreverse, .CurveEaseInOut], animations: { () -> Void in
self.imageView.transform = rotate
}, completion: nil)
}
}
But it doesn't work well.
Your transforms obviously dont fit your requirements.
If you want image to scale proportionally, why you set different scales for X and Y then?
Again, if you want image to move along X axis, why you adjust Y position of it in transforms? Simply fix your transform and you are ready to go.
In particular:
let scale = CGAffineTransformMakeScale(3.0, 3.0)
...
let translate = CGAffineTransformMakeTranslation(UIScreen.mainScreen().bounds.width - 400, 0)
Here is my answer:
import UIKit
import QuartzCore
class ViewController: UIViewController {
#IBOutlet var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
let kRotationAnimationKey = "com.myapplication.rotationanimationkey"
func rotateView(view: UIView, duration: Double = 1) {
if view.layer.animationForKey(kRotationAnimationKey) == nil {
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Float(M_PI * 2.0)
rotationAnimation.duration = duration
rotationAnimation.repeatCount = Float.infinity
view.layer.addAnimation(rotationAnimation, forKey: kRotationAnimationKey)
}
}
func stopRotatingView(view: UIView) {
if view.layer.animationForKey(kRotationAnimationKey) != nil {
view.layer.removeAnimationForKey(kRotationAnimationKey)
}
}
var transform = CGAffineTransformMakeTranslation(UIScreen.mainScreen().bounds.width - 200, 0)
transform = CGAffineTransformScale(transform, 3.0, 3.0)
UIView.animateWithDuration(1.0, delay: 0, options: [.Repeat, .Autoreverse, .CurveEaseInOut], animations: { () -> Void in
self.imageView.transform = transform
rotateView(self.imageView)
self.view.layoutIfNeeded()
}) { (_) -> Void in
stopRotatingView(self.imageView)
}
}
}
Result:
Related
I’m wanting to do successive animations, like one after another instead of on a loop or one at a time. What does it look like when coding that?
Example: fade in # 100% opacity, then fade out # 20% opacity, then fade in 80%, then fade out 10%... so like a pulsing then at 0% change the label text and do the inverse (basically the same as the picture only every time I try and make it progressively fade out- it disrupts the whole animation)
The gif shows it's current state, not what I've tried thus far that didnt work.
import UIKit
class Belief2ViewController: UIViewController {
#IBOutlet var negBeliefLabelGlower: UILabel!
#IBOutlet var posBeliefLabelGlower: UILabel!
#IBOutlet var labelNegBeliefFinal: UILabel!
#IBOutlet var startButton: UIButton!
//PASSING INFO
var negBeliefLabelGlowerText = String()
var posBeliefLabelGlowerText = String()
override func viewDidLoad() {
super.viewDidLoad()
//PASSING INFO
negBeliefLabelGlower.text = negBeliefLabelGlowerText
posBeliefLabelGlower.text = posBeliefLabelGlowerText
//PASSING INFO
labelNegBeliefFinal.text = negBeliefLabelGlowerText
//GLOW
labelNegBeliefFinal.UILableTextShadow(color: UIColor.systemTeal)
//AniamtionOpacityLOOP
labelNegBeliefFinal.alpha = 0.3
// UIView.animate(withDuration: 5, animations: {self.labelNegBeliefFinal.alpha = 100}, completion: { _ in self.labelNegBeliefFinal.alpha = 90;})
UIView.animate(withDuration: 6, delay: 0, options: [.autoreverse, .repeat], animations: { self.labelNegBeliefFinal.alpha = 100 })
//AnimationBreathSizeLOOP
UIView.animate(withDuration: 6, delay: 0, options: [.autoreverse, .repeat], animations: { self.labelNegBeliefFinal.transform = CGAffineTransform(scaleX: 1.3, y: 1.3) })
}
#IBAction func startButtonPress(_ sender: Any) {
//self.labelNegBeliefFinal.text = self.posBeliefLabelGlowerText
// UIView.animate(withDuration: 10, animations: {self.labelNegBeliefFinal.alpha = 0}, completion: { _ in self.labelNegBeliefFinal.text = self.posBeliefLabelGlowerText;})
UIView.animate(withDuration: 10, delay: 0, animations: {self.labelNegBeliefFinal.alpha = 0}, completion: { _ in
self.labelNegBeliefFinal.text = self.posBeliefLabelGlowerText ;})
//
}
}
//GLOW
extension UILabel {
func UILableTextShadow1(color: UIColor){
textColor = UIColor.systemTeal
layer.shadowColor = UIColor.systemTeal.cgColor
layer.masksToBounds = false
layer.shadowOffset = .zero
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
layer.shadowRadius = 5.0
layer.shadowOpacity = 5.0
}
}
EDIT
Okay so using the keyframes as suggest was a perfect suggestion
but I am running into a bit of an issue. I cannot key in the label text to change
UIView.animateKeyframes(withDuration: 15.0,
delay: 0.0,
options: [],
animations: {
UIView.addKeyframe(withRelativeStartTime: 1,
relativeDuration: 0.0,
animations: { self.labelNegBeliefFinal.text = self.posBeliefLabelGlowerText })
},
completion: nil)
So I wasn't able to incorporate the label into the keyframe animation so I just set a delay to the bale changing and it took a little tweaking but here is what I came up with
//CHANGE LABEL ON TIMER
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 13.5)
{ [self] in
self.labelNegBeliefFinal.text = self.posBeliefLabelGlowerText
}
Here is the glow loop and size loop as before, unrelated to the tween
//AniamtionGlowLOOP
UIView.animate(withDuration: 6, delay: 0, options: [.autoreverse, .repeat],
animations: { self.labelNegBeliefFinal.layer.shadowOpacity = 5.0 })
//AnimationBreathSizeLOOP
UIView.animate(withDuration: 6, delay: 0, options: [.autoreverse, .repeat],
animations: { self.labelNegBeliefFinal.transform = CGAffineTransform(scaleX:
1.3, y: 1.3) })
// }
Here is the tween code I came up with thanks to Matt for the point in the right direction. Had no idea you could tween in SwiftUI. One thing that threw me off is that if the duration is 30sec, relative duration being set to 0.5 is equal to 15 seconds as it's 0.5 of 30 seconds. Some of my tweens didn't seem to work because I didn't realize that.
UIView.animateKeyframes(withDuration: 30.0,
delay: 0,
options: [ ],
animations: {
UIView.addKeyframe(withRelativeStartTime:
0,
relativeDuration: 0.5,
animations: { self.labelNegBeliefFinal.alpha = 0 })
UIView.addKeyframe(withRelativeStartTime:
0.5,
relativeDuration: 0.5,
animations: { self.labelNegBeliefFinal.alpha = 1 })
},
completion: nil)
}
}
Glow style I got from a different post
//GLOW
extension UILabel {
func UILableTextShadow1(color: UIColor){
textColor = UIColor.systemTeal
layer.shadowColor = UIColor.systemTeal.cgColor
layer.masksToBounds = false
layer.shadowOffset = .zero
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
layer.shadowRadius = 5.0
layer.shadowOpacity = 00.7
}
}
I am trying to create an app home screen animation from splash, like after launch screen completed (full)screen color transforms into an app logo background color. Currently below code kind of archive what I expected. But, that transformation CAShapeLayer doesn't do with corner radius. Without corner radius it works as normal, when I try to use circle/oval/corner radius animation seems like below gif.
Tried few other StackOverflow answers which create circle animation those are not working. Here one of those.
weak var viewTransitionContext: UIViewControllerContextTransitioning!
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
viewTransitionContext = transitionContext
guard let fromVC = viewTransitionContext.viewController(forKey: .from) else { return }
guard let toVC = viewTransitionContext.viewController(forKey: .to) else { return }
if fromVC.isKind(of: SOGSplashViewController.self) && toVC.isKind(of: SOGHomeViewController.self) {
guard let toVCView = transitionContext.view(forKey: .to) else { return }
guard let fromVCView = transitionContext.view(forKey: .from) else { return }
let containerView = transitionContext.containerView
let labelWidth = UIDevice.width() * 0.75
let labelHeight = labelWidth * 0.7
let xAxis = (UIDevice.width() - labelWidth)/2.0
let yAxis = ((UIDevice.height()/2.0) - labelHeight)/2.0
let labelRect = CGRect(x: xAxis, y: yAxis, width: labelWidth, height: labelHeight)
let radius = (UIDevice.height()/2.0)*0.1
let fromFrame = fromVCView.bounds
let animationTime = transitionDuration(using: transitionContext)
let maskLayer = CAShapeLayer()
maskLayer.isOpaque = false
maskLayer.fillColor = fromVCView.backgroundColor?.cgColor
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.path = toPathValue.cgPath
let maskAnimationLayer = CABasicAnimation(keyPath: "path")
maskAnimationLayer.fromValue = (UIBezierPath(rect: fromFrame)).cgPath
maskAnimationLayer.toValue = toPathValue.cgPath
maskAnimationLayer.duration = animationTime
maskAnimationLayer.delegate = self as? CAAnimationDelegate
containerView.addSubview(fromVCView)
containerView.addSubview(toVCView)
fromVCView.layer.add(maskAnimationLayer, forKey: nil)
maskLayer.add(maskAnimationLayer, forKey: "path")
containerView.layer.addSublayer(maskLayer)
let deadLineTime = DispatchTime.now() + .seconds(1)
DispatchQueue.main.asyncAfter(deadline: deadLineTime) {
UIView.animate(withDuration: 0.2, animations: {
maskLayer.opacity = 0
}, completion: { (isSuccess) in
self.viewTransitionContext.completeTransition(true)
})
}
}
}
Transforming a rectangular path to a rounded rectangular path is a very complex operation if you do it through a generic way like Core Animation.. You should better use the cornerRadius property of CALayer which is animatable.
Here is a working example with a constraint based animation:
class ViewController: UIViewController {
#IBOutlet var constraints: [NSLayoutConstraint]!
#IBOutlet var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
self.contentView.layer.cornerRadius = 10.0
self.animate(nil)
}
#IBAction func animate(_ sender: Any?) {
for c in constraints {
c.constant = 40.0
}
UIView.animate(withDuration: 4.0) {
self.view.layoutIfNeeded()
self.contentView.layer.cornerRadius = 40.0
}
}
}
contentView points to the inner view which should be animated, and constraints refer to the four layout constraints defining the distances from the view controller's view to the content view.
This is just a simple, rough example, which can certainly be improved.
I'm trying to link multiple buttons with the same IBAction, to run similar but different code. The code is to set an image that was clicked on another view controller into the UIImageView under the button.
All the buttons link to the same view controller but with a different segue.
I tried to write a if statements but I didn't seem to have it right. I have named each corresponding UIImage view: technologyImageViewTwo, technologyImageViewThree ...etc
below is the code I used for the first button which works with the corresponding UIImageView named technologyImageView
#IBAction func setTechnology(segue:UIStoryboardSegue) {
dismiss(animated: true) {
if let technology = segue.identifier{
self.persona.technology = technology
self.technologyView.technologyImageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
self.technologyView.technologyImageView.transform = scaleUp
self.technologyView.technologyImageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
self.technologyView.technologyImageView.transform = .identity
self.technologyView.technologyImageView.alpha = 1
}, completion: nil)
}
I expect that each button should go to the segued view controller and the image selected will show up under the corresponding button. E.g if I click on the button 'Technology 2' and choose an image, the image shows up in the UIImageview named technologyImageViewTwo.
There are a couple of options, that you can choose from:
Option 1:
This option would be more preferred, that is to use the tag property on the component, this will allow you to identify the index of the button when it has been actioned.
https://developer.apple.com/documentation/uikit/uiview/1622493-tag
#IBAction func action(_ sender: Any) {
dismiss(animated: true) {
var imageView: UIImageView!
let index = (sender as? UIView)?.tag ?? 0
switch index {
case 1:
persona.technology = <#T##String#>
imageView = technologyView.technologyImageViewTwo
case 2:
persona.technology = <#T##String#>
imageView = technologyView.technologyImageViewThree
default:
persona.technology = <#T##String#>
imageView = technologyView.technologyImageView
}
if let technology = persona.technology {
imageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
imageView.transform = scaleUp
imageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
imageView.transform = .identity
imageView.alpha = 1
})
}
}
Option 2:
If you want to continue with the code you have then you could use a segue identifier that contains components that you can be split out and enable you to identify them more efficiently:
segueIdentifier-1, segueIdentifier-2, segueIdentifier-3
func setTechnology(segue: UIStoryboardSegue) {
dismiss(animated: true) {
var imageView: UIImageView!
let identifierComponents = segue.identifier?.components(separatedBy: "-")
let index = Int(identifierComponents?.last ?? "0")
switch index {
case 1:
imageView = technologyView.technologyImageViewTwo
case 2:
imageView = technologyView.technologyImageViewThree
default:
imageView = technologyView.technologyImageView
}
if let technology = identifierComponents?.first {
self.persona.technology = technology
imageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
imageView.transform = scaleUp
imageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
imageView.transform = .identity
imageView.alpha = 1
})
}
}
You could use three separate IBActions that all call the same function with the UIImageView number as a parameter.
func setTechnology(segue:UIStoryboardSegue, imageViewNumber: Int) {
dismiss(animated: true) {
var imageView: UIImageView!
if imageViewNumber == 0 {
imageView = self.technologyView.technologyImageView
} else if imageView == 1 {
imageView = self.technologyView.technologyImageViewTwo
} else {
imageView = self.technologyView.technologyImageViewThree
}
if let technology = segue.identifier{
self.persona.technology = technology
imageView.image = UIImage(named: technology)
}
//animating scale up of image
let scaleUp = CGAffineTransform.init(scaleX: 0.1, y:0.1)
imageView.transform = scaleUp
imageView.alpha = 0
//animating bounce effect
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.7, options: [], animations: {
imageView.transform = .identity
imageView.alpha = 1
}, completion: nil)
}
And then call this function from inside the three separate IBActions.
#IBAction func tappedButtonZero(segue:UIStoryboardSegue) {
self.setTechnology(segue: segue, imageViewNumber: 0)
}
#IBAction func tappedButtonOne(segue:UIStoryboardSegue) {
self.setTechnology(segue: segue, imageViewNumber: 1)
}
#IBAction func tappedButtonTwo(segue:UIStoryboardSegue) {
self.setTechnology(segue: segue, imageViewNumber: 2)
}
You could drag multiple buttons to your #IBAction and assign a unique tag value to each button. Then, use a switch statement to do whatever's unique to the button you pressed.
#IBAction func tappedButton(_ sender: UIButton) {
switch sender.tag {
case 1:
print("one")
case 2:
print("two")
case 3:
print("three")
default:
break
}
}
I've created a heart beat animation for a UIButton. However, there is no way to stop this animation as it's an endless code loop. After tinkering with numerous UIView animation code blocks I've been unable to get UIViewAnimationOptions.Repeat to produce what I need. If I could do that I could simply button.layer.removeAllAnimations() to remove the animations. What is a way to write this that allows for removal of the animation? I'm thinking a timer possibly but that could be kind of messy with multiple animations going on.
func heartBeatAnimation(button: UIButton) {
button.userInteractionEnabled = true
button.enabled = true
func animation1() {
UIView.animateWithDuration(0.5, delay: 0.0, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}, completion: nil)
UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}) { (Bool) -> Void in
delay(2.0, closure: { () -> () in
animation2()
})
}
}
func animation2() {
UIView.animateWithDuration(0.5, delay: 0.0, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}, completion: nil)
UIView.animateWithDuration(0.5, delay: 0.5, options: [], animations: { () -> Void in
button.transform = CGAffineTransformMakeScale(2.0, 2.0)
button.transform = CGAffineTransformIdentity
}) { (Bool) -> Void in
delay(2.0, closure: { () -> () in
animation1()
})
}
}
animation1()
}
This works perfectly. The damping and spring need to be tweaked a little bit but this solves the problem. removeAllAnimations() clears the animation and returns the button to it's normal state.
button.userInteractionEnabled = true
button.enabled = true
let pulse1 = CASpringAnimation(keyPath: "transform.scale")
pulse1.duration = 0.6
pulse1.fromValue = 1.0
pulse1.toValue = 1.12
pulse1.autoreverses = true
pulse1.repeatCount = 1
pulse1.initialVelocity = 0.5
pulse1.damping = 0.8
let animationGroup = CAAnimationGroup()
animationGroup.duration = 2.7
animationGroup.repeatCount = 1000
animationGroup.animations = [pulse1]
button.layer.addAnimation(animationGroup, forKey: "pulse")
This post was very helpful: CAKeyframeAnimation delay before repeating
Swift 5 code, works without the pause between pulses:
let pulse = CASpringAnimation(keyPath: "transform.scale")
pulse.duration = 0.4
pulse.fromValue = 1.0
pulse.toValue = 1.12
pulse.autoreverses = true
pulse.repeatCount = .infinity
pulse.initialVelocity = 0.5
pulse.damping = 0.8
switchButton.layer.add(pulse, forKey: nil)
I created something like this:
let animation = CAKeyframeAnimation(keyPath: "transform.scale")
animation.values = [1.0, 1.2, 1.0]
animation.keyTimes = [0, 0.5, 1]
animation.duration = 1.0
animation.repeatCount = Float.infinity
layer.add(animation, forKey: "pulse")
On your original question you mentioned that you want the animation to stop on command. I assume you would like it to start on command too. This solution will do both and it is quite simple.
func cutAnim(){
for view in animating {
///I use a UIView because I wanted the container of my button to be animated. UIButton will work just fine too.
(view.value as? UIView)?.layer.removeAllAnimations()
}
}
func pulse(button: UIButton, name: String){
///Here I capture that container
let container = button.superview?.superview
///Add to Dictionary
animating[name] = container
cutAnim()
UIView.animate(withDuration: 1, delay: 0.0, options:[UIViewAnimationOptions.repeat, UIViewAnimationOptions.autoreverse, .allowUserInteraction], animations: {
container?.transform = CGAffineTransform(scaleX: 1.15, y: 1.15)
///if you stop the animation half way it completes anyways so I want the container to go back to its original size
container?.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
}, completion: nil)
}
Call cutAnim() anywhere to stop an animation, inside a timer if you want.
To start the animation use a regular button action
#IBAction func buttonWasTappedAction(_ sender: Any) {
pulse(button: sender as! UIButton, name: "nameForDictionary")
}
Hope this helps.
Here's my Code for my Custom Segue
class FlipFromRightSegue: UIStoryboardSegue {
override func perform() {
let source:UIViewController = self.sourceViewController as UIViewController
let destination:UIViewController = self.destinationViewController as UIViewController
UIView.transitionWithView(source.view, duration: 1.0, options: .CurveEaseInOut | .TransitionFlipFromRight, animations: { () -> Void in
source.view.addSubview(destination.view)
}) { (finished) -> Void in
destination.view.removeFromSuperview()
source.presentViewController(destination, animated: false, completion: nil)
}
}
}
I thought this works but actually the view changes only when the segue is already performed. What should I do so that the view changes when the "Flip" is in the middle?
Thanks in advance.
As of iOS 7, we generally don't animate transitions using a custom segue. We'd either use a standard modal presentation, specifying a modalTransitionStyle (i.e. a fixed list of a few animations we can pick for our modal transitions), or you'd implement custom animation transitions. Both of those are described below:
If you are simply presenting another view controller's view, the simple solution for changing the animation to a flip is by setting the modalTransitionStyle in the destination view controller. You can do this entirely in Interface Builder under the segue's properties.
If you want to do it programmatically, in the destination controller you could do the following in Swift 3:
override func viewDidLoad() {
super.viewDidLoad()
modalTransitionStyle = .flipHorizontal // use `.FlipHorizontal` in Swift 2
}
Then, when you call show/showViewController or present/presentViewController, and your presentation will be performed with horizontal flip. And, when you dismiss the view controller, the animation is reversed automatically for you.
If you need more control, in iOS 7 and later, you would use custom animation transitions, in which you'd specify a modalPresentationStyle of .custom. For example, in Swift 3:
class SecondViewController: UIViewController {
let customTransitionDelegate = TransitioningDelegate()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
modalPresentationStyle = .custom // use `.Custom` in Swift 2
transitioningDelegate = customTransitionDelegate
}
...
}
That specifies the UIViewControllerTransitioningDelegate that would instantiate the animation controller. For example, in Swift 3:
class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return AnimationController(transitionType: .presenting)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return AnimationController(transitionType: .dismissing)
}
}
And the animation controller would simply do .transitionFlipFromRight is a presentation, or .transitionFlipFromLeft if dismissing in Swift 3:
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case presenting
case dismissing
}
var animationTransitionOptions: UIViewAnimationOptions
init(transitionType: TransitionType) {
switch transitionType {
case .presenting:
animationTransitionOptions = .transitionFlipFromRight
case .dismissing:
animationTransitionOptions = .transitionFlipFromLeft
}
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
//let inView = transitionContext.containerView
let toView = transitionContext.viewController(forKey: .to)?.view
let fromView = transitionContext.viewController(forKey: .from)?.view
UIView.transition(from: fromView!, to: toView!, duration: transitionDuration(using: transitionContext), options: animationTransitionOptions.union(.curveEaseInOut)) { finished in
transitionContext.completeTransition(true)
}
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
}
For more information on the custom transitions introduced in iOS 7, see WWDC 2013 video Custom Transitions Using View Controllers.
If should be acknowledged that the above AnimationController is actually a over-simplification, because we're using transform(from:to:...). That results in an animation that isn't cancelable (in case you're using interactive transition). It's also removing the "from" view itself, and as of iOS 8, that's now really the job of the presentation controller.
So, you really want to do the flip animation using UIView.animate API. I apologize because the following involves using some unintuitive CATransform3D in key frame animations, but it results in a flip animation that can then be subjected to cancelable interactive transitions.
So, in Swift 3:
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
enum TransitionType {
case presenting
case dismissing
}
let transitionType: TransitionType
init(transitionType: TransitionType) {
self.transitionType = transitionType
super.init()
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let inView = transitionContext.containerView
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
var frame = inView.bounds
func flipTransform(angle: CGFloat, offset: CGFloat = 0) -> CATransform3D {
var transform = CATransform3DMakeTranslation(offset, 0, 0)
transform.m34 = -1.0 / 1600
transform = CATransform3DRotate(transform, angle, 0, 1, 0)
return transform
}
toView.frame = inView.bounds
toView.alpha = 0
let transformFromStart: CATransform3D
let transformFromEnd: CATransform3D
let transformFromMiddle: CATransform3D
let transformToStart: CATransform3D
let transformToMiddle: CATransform3D
let transformToEnd: CATransform3D
switch transitionType {
case .presenting:
transformFromStart = flipTransform(angle: 0, offset: inView.bounds.size.width / 2)
transformFromEnd = flipTransform(angle: -.pi, offset: inView.bounds.size.width / 2)
transformFromMiddle = flipTransform(angle: -.pi / 2)
transformToStart = flipTransform(angle: .pi, offset: -inView.bounds.size.width / 2)
transformToMiddle = flipTransform(angle: .pi / 2)
transformToEnd = flipTransform(angle: 0, offset: -inView.bounds.size.width / 2)
toView.layer.anchorPoint = CGPoint(x: 0, y: 0.5)
fromView.layer.anchorPoint = CGPoint(x: 1, y: 0.5)
case .dismissing:
transformFromStart = flipTransform(angle: 0, offset: -inView.bounds.size.width / 2)
transformFromEnd = flipTransform(angle: .pi, offset: -inView.bounds.size.width / 2)
transformFromMiddle = flipTransform(angle: .pi / 2)
transformToStart = flipTransform(angle: -.pi, offset: inView.bounds.size.width / 2)
transformToMiddle = flipTransform(angle: -.pi / 2)
transformToEnd = flipTransform(angle: 0, offset: inView.bounds.size.width / 2)
toView.layer.anchorPoint = CGPoint(x: 1, y: 0.5)
fromView.layer.anchorPoint = CGPoint(x: 0, y: 0.5)
}
toView.layer.transform = transformToStart
fromView.layer.transform = transformFromStart
inView.addSubview(toView)
UIView.animateKeyframes(withDuration: self.transitionDuration(using: transitionContext), delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.0) {
toView.alpha = 0
fromView.alpha = 1
}
UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.5) {
toView.layer.transform = transformToMiddle
fromView.layer.transform = transformFromMiddle
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.0) {
toView.alpha = 1
fromView.alpha = 0
}
UIView.addKeyframe(withRelativeStartTime: 0.5, relativeDuration: 0.5) {
toView.layer.transform = transformToEnd
fromView.layer.transform = transformFromEnd
}
}, completion: { finished in
toView.layer.transform = CATransform3DIdentity
fromView.layer.transform = CATransform3DIdentity
toView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
fromView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
}
FYI, iOS 8 extends the custom transition model with presentation controllers. For more information, see WWDC 2014 video A Look Inside Presentation Controllers.
Anyway, if, at the end of the transition, the "from" view is no longer visible, you'd instruct your presentation controller to remove it from the view hierarchy, e.g.:
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
And, obviously, you have to inform your TransitioningDelegate of this presentation controller:
class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
...
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
}
This answer has been updated for Swift 3. Please refer to the previous revision of this answer if you want to see the Swift 2 implementation.