iOS - Card flip animation
Your problem is that your UIImageViews are directly on the "full page" view.
transitionFromView removes the fromView from its superview and adds the toView on the superview with the given animation. Thus, it animates the superview.
You should include a UIView that servers as a container and have both imageViews as subviews. Add your tap gesture on the containerview. Also, you should not have weak references to the imageViews, since once you have done the animation once, your reference to the back imageView will be gone. It is probably better to add these in code rather than storyboard. No need to hide the imageViews.
Here are some sample code:
class MyViewController: UIViewController {
@IBOutlet weak var containerView: UIView!
private let backImageView: UIImageView! = UIImageView(image: UIImage(named: "back"))
private let frontImageView: UIImageView! = UIImageView(image: UIImage(named: "front"))
private var showingBack = false
override func viewDidLoad() {
super.viewDidLoad()
frontImageView.contentMode = .ScaleAspectFit
backImageView.contentMode = .ScaleAspectFit
containerView.addSubview(frontImageView)
frontImageView.translatesAutoresizingMaskIntoConstraints = false
frontImageView.spanSuperview()
let singleTap = UITapGestureRecognizer(target: self, action: #selector(flip))
singleTap.numberOfTapsRequired = 1
containerView.addGestureRecognizer(singleTap)
}
func flip() {
let toView = showingBack ? frontImageView : backImageView
let fromView = showingBack ? backImageView : frontImageView
UIView.transitionFromView(fromView, toView: toView, duration: 1, options: .TransitionFlipFromRight, completion: nil)
toView.translatesAutoresizingMaskIntoConstraints = false
toView.spanSuperview()
showingBack = !showingBack
}
}
Card flipping animation
In order to create a basic card flipping animation like the one in the video you've linked to, I suggest putting frontImageView
and the backImageView
directly on top of each other on the UIView
you intend to flip. To start, set their images to front and back accordingly; and, in this particular case, hide the frontImageView
and show the backImageView
.
Assuming "card" is the UIView
you intend to flip, to perform the flip try:
[UIView transitionWithView:card duration:0.65f
options:UIViewAnimationOptionTransitionFlipFromRight animations:^{
frontImageView.hidden = NO;
backImageView.hidden = YES;
} completion:^(BOOL finished) {
// whatever you'd like to do immediately after the flip completes
}];
Edit:
And to handle the shadow, first off, it appears in the video you posted that the shadow grows in length moreso than it just fades in. And it seems as if (and makes logical sense that) the shadow reaches its peak during the middle of the animation as the card is lifted at its highest point. Since the shadow grows then shrinks during the course of the flip animation, it doesn't make sense to include the shadow animation within the same animation block as the flip since they're on different time schedules.
Secondly with regard to the shadow, to animate the layer property, you have to use Core Animations.
Perhaps you can run the two animations concurrently, i.e. while the above animation is performing, also do something like:
CABasicAnimation *shadowAnimation = [CABasicAnimation animationWithKeyPath:@"shadowRadius"];
shadowAnimation.delegate = self;
[shadowAnimation setFromValue:[NSNumber numberWithFloat:3.0]];
[shadowAnimation setToValue:[NSNumber numberWithFloat:10.0]];
[shadowAnimation setDuration:0.65f];
shadowAnimation.autoreverses = YES;
[[card layer] addAnimation:shadowAnimation forKey:@"shadowRadius"];
The last portion has been adapted from this code and takes advantage of the autoreverse property to automatically reverse the shadow's growth.
SwiftUI Card flip with two views
Simple Solution
The approach you're taking can be made to work by putting your two views in a ZStack and then showing/hiding them as the flipped
state changes. The rotation of the second view needs to be offset. But this solution relies on a cross-fade between the two views. It might be OK for some uses cases. But there is a better solution - though it's a bit more fiddly (see below).
Here's a way to make your approach work:
struct SimpleFlipper : View {
@State var flipped = false
var body: some View {
let flipDegrees = flipped ? 180.0 : 0
return VStack{
Spacer()
ZStack() {
Text("Front").placedOnCard(Color.yellow).flipRotate(flipDegrees).opacity(flipped ? 0.0 : 1.0)
Text("Back").placedOnCard(Color.blue).flipRotate(-180 + flipDegrees).opacity(flipped ? 1.0 : 0.0)
}
.animation(.easeInOut(duration: 0.8))
.onTapGesture { self.flipped.toggle() }
Spacer()
}
}
}
extension View {
func flipRotate(_ degrees : Double) -> some View {
return rotation3DEffect(Angle(degrees: degrees), axis: (x: 1.0, y: 0.0, z: 0.0))
}
func placedOnCard(_ color: Color) -> some View {
return padding(5).frame(width: 250, height: 150, alignment: .center).background(color)
}
}
Better Solution SwiftUI has some useful animation tools - such as GeometryEffect - that can generate a really smooth version of this effect. There are some excellent blog posts on this topic at SwiftUI Lab. In particular, see: https://swiftui-lab.com/swiftui-animations-part2/
I've simplified and adapted one of examples in that post to provide the card flipping functionality.
struct FlippingView: View {
@State private var flipped = false
@State private var animate3d = false
var body: some View {
return VStack {
Spacer()
ZStack() {
FrontCard().opacity(flipped ? 0.0 : 1.0)
BackCard().opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: animate3d ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.animate3d.toggle()
}
}
Spacer()
}
}
}
struct FlipEffect: GeometryEffect {
var animatableData: Double {
get { angle }
set { angle = newValue }
}
@Binding var flipped: Bool
var angle: Double
let axis: (x: CGFloat, y: CGFloat)
func effectValue(size: CGSize) -> ProjectionTransform {
DispatchQueue.main.async {
self.flipped = self.angle >= 90 && self.angle < 270
}
let tweakedAngle = flipped ? -180 + angle : angle
let a = CGFloat(Angle(degrees: tweakedAngle).radians)
var transform3d = CATransform3DIdentity;
transform3d.m34 = -1/max(size.width, size.height)
transform3d = CATransform3DRotate(transform3d, a, axis.x, axis.y, 0)
transform3d = CATransform3DTranslate(transform3d, -size.width/2.0, -size.height/2.0, 0)
let affineTransform = ProjectionTransform(CGAffineTransform(translationX: size.width/2.0, y: size.height / 2.0))
return ProjectionTransform(transform3d).concatenating(affineTransform)
}
}
struct FrontCard : View {
var body: some View {
Text("One thing is for sure – a sheep is not a creature of the air.").padding(5).frame(width: 250, height: 150, alignment: .center).background(Color.yellow)
}
}
struct BackCard : View {
var body: some View {
Text("If you know you have an unpleasant nature and dislike people, this is no obstacle to work.").padding(5).frame(width: 250, height: 150).background(Color.green)
}
}
Update
The OP asks about managing the flip status outside of the view. This can be done by using a binding. Below is a fragment that implements and demos this. And OP also asks about flipping with and without animation. This is a matter of whether changing the flip state (here with the showBack
var) is done within an animation block or not. (The fragment doesn't include FlipEffect
struct which is just the same as the code above.)
struct ContentView : View {
@State var showBack = false
let sample1 = "If you know you have an unpleasant nature and dislike people, this is no obstacle to work."
let sample2 = "One thing is for sure – a sheep is not a creature of the air."
var body : some View {
let front = CardFace(text: sample1, background: Color.yellow)
let back = CardFace(text: sample2, background: Color.green)
let resetBackButton = Button(action: { self.showBack = true }) { Text("Back")}.disabled(showBack == true)
let resetFrontButton = Button(action: { self.showBack = false }) { Text("Front")}.disabled(showBack == false)
let animatedToggle = Button(action: {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}) { Text("Toggle")}
return
VStack() {
HStack() {
resetFrontButton
Spacer()
animatedToggle
Spacer()
resetBackButton
}.padding()
Spacer()
FlipView(front: front, back: back, showBack: $showBack)
Spacer()
}
}
}
struct FlipView<SomeTypeOfViewA : View, SomeTypeOfViewB : View> : View {
var front : SomeTypeOfViewA
var back : SomeTypeOfViewB
@State private var flipped = false
@Binding var showBack : Bool
var body: some View {
return VStack {
Spacer()
ZStack() {
front.opacity(flipped ? 0.0 : 1.0)
back.opacity(flipped ? 1.0 : 0.0)
}
.modifier(FlipEffect(flipped: $flipped, angle: showBack ? 180 : 0, axis: (x: 1, y: 0)))
.onTapGesture {
withAnimation(Animation.linear(duration: 0.8)) {
self.showBack.toggle()
}
}
Spacer()
}
}
}
struct CardFace<SomeTypeOfView : View> : View {
var text : String
var background: SomeTypeOfView
var body: some View {
Text(text)
.multilineTextAlignment(.center)
.padding(5).frame(width: 250, height: 150).background(background)
}
}
Want to flip two views so that one view hides and other shows swift
UIView.transition(with
applies to a container view so you can try putting your subviews in a container view and apply UIView.transition
on container view. Because you havent added any code am assuming few things here to answer
class ViewController: UIViewController {
@IBOutlet weak var button: UIButton!
let view1 = UIView()
let view2 = UIView()
let containerView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
view1.backgroundColor = UIColor.red
view2.backgroundColor = UIColor.green
view.addSubview(containerView)
containerView.addSubview(view1)
containerView.addSubview(view2)
containerView.translatesAutoresizingMaskIntoConstraints = false
view1.translatesAutoresizingMaskIntoConstraints = false
view2.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
containerView.topAnchor.constraint(equalTo: view.topAnchor, constant: 100),
view1.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
view1.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
view1.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
view1.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 100),
view2.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
view2.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
view2.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
view2.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 100)
])
view1.isHidden = false
view2.isHidden = true
}
@IBAction func changeTapped() {
UIView.transition(with: containerView,
duration: 1.0,
options: .transitionFlipFromBottom, animations: {[weak self] in
guard let self = self else { return }
self.view1.isHidden = !self.view1.isHidden
self.view2.isHidden = !self.view2.isHidden
})
}
O/P:
Can't get card flip animation to work
The views you want to transition between should be contained within another view, see the following hierarchy:
The transition should then be applied to the "Transition View":
UIView.transitionWithView(self.transitionView, duration: 1.5, options: .TransitionFlipFromRight, animations:{
self.frontImageView.hidden = self.showingImage;
self.behindView.hidden = !self.showingImage;
}) { (complete) -> Void in
self.showingImage = !self.showingImage
}
self.showingImage
is a simple Bool
instance variable to keep track of which view is currently being shown.
Related Topics
How to Set Cmutablepointer<Objcbool> to False in Swift
How to Automatically Create an Initializer for a Swift Class
Get Latitude and Longitude Center of Google Map
Swift Get Specific Value from Firebase Database
Correct Way to Layout Swiftui (Similar to Autolayout)
Apple Watch Table - First 4 Rows Not Appearing
Swift How to "Pass by Value" of a Object
How to Update Constraints Programmatically
Removing a View Controller from Memory When Instantiating a New View Controller
Returning Data from Function in Firebase Observer Code Block Swift
Swift Core Data Sync with Web Server
Avcapturestillimageoutput VS Avcapturephotooutput in Swift 3
iOS with Parse. Pfuser.Currentuser() Not Getting Cached. Returns Nil After App Restart
How to Keep the Header Cell Moving with the Tableview Cells in Swift 2.0
Blur Effect - Background Uitextfield
Swift Sending Data from Child View to Parent View Controller