Reputation: 75
I am using a UIView.transition to flip a card over.
While troubleshooting it not working, I stumbled upon a way to make it work - but I have no idea why. I am hoping that someone can look at the two code blocks below and help me understand why one works and the other doesn't. It seems very strange to me.
First, here is the code block that actually works. The card flip is visually flawless.
UIView.animate(withDuration: 0.01) { imageView.alpha = 1.0 imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded() } completion: { (true) in UIView.transition(with: imageView, duration: 1.2, options: animation) { imageView.image = endingImage imageView.layoutIfNeeded() // Works with and without this layoutIfNeeded() } completion: { (true) in if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 } UIView.animate(withDuration: 0.01) { self.coveringLabel.backgroundColor = .clear self.coveringLabel.layoutIfNeeded() imageView.removeFromSuperview() self.tickNumLabel.alpha = originalTicketNumAlpha } } }
But I don't understand why I seem to need to wrap the UIView.transition() into the completion handler of a call to UIView.animate() in order for the flip animation to work.
*(Note: If I pull the "imageView.alpha = 1.0" out of the animate() block and place it immediately BEFORE calling UIView.animate() - the flip animation does not occur (with or without the layoutIfNeeded() call). It just toggles the images. *
Now, here is the code that I expected to work - but when I use this code instead of the above code, there is no "flip" transition. The card image just immediately toggles between the face up and face down image. The "UIView.transition" call here is identical to the one in the above code. The only difference here is that it's NOT wrapped into a 0.01 second UIView.animate completion block.
imageView.alpha = 1.0 imageView.layoutIfNeeded() UIView.transition(with: imageView, duration: 1.2, options: animation) { imageView.image = endingImage imageView.layoutIfNeeded() // same behaviour with and without this line } completion: { (true) in if self.dealTicketState.isTicketFaceUp == true { self.faceDownView.alpha = 0.0 } else { self.faceDownView.alpha = 1.0 } UIView.animate(withDuration: 0.01) { self.coveringLabel.backgroundColor = .clear self.coveringLabel.layoutIfNeeded() imageView.removeFromSuperview() self.tickNumLabel.alpha = originalTicketNumAlpha } }
This transition ends my flipTicket() function. The code that precedes this transition is identical in both cases. I wasn't going to include it because I don't think it's necessary to understand the problem - but then again - what do I know? Here's the stuff that came before the above clips:
func flipTicket() { let originalTicketNumAlpha = self.tickNumLabel.alpha self.tickNumLabel.alpha = 0.0 let tempFaceDownImage:UIImage = self.dealTicketState.faceDownImage let tempFaceUpImage:UIImage = getCurrentFaceUpImage() var endingImage:UIImage = self.dealTicketState.faceDownImage let imageView = UIImageView() imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .scaleToFill imageView.clipsToBounds = true imageView.alpha = 0.0 self.coveringLabel.alpha = 1.0 self.coveringLabel.backgroundColor = .black self.coveringLabel.layoutIfNeeded() var animation:UIView.AnimationOptions = .transitionFlipFromLeft if faceDownView.alpha == 1.0 { animation = .transitionFlipFromRight imageView.image = tempFaceDownImage endingImage = tempFaceUpImage } else { animation = .transitionFlipFromLeft imageView.image = tempFaceUpImage } self.addSubview(imageView) NSLayoutConstraint.activate([ imageView.topAnchor.constraint(equalTo: self.topAnchor), imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor), imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor), imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor), ]) imageView.layoutIfNeeded()
Background: This code is part of a custom UI control which represents a playing card. It consists of several subviews. The topmost subview was originally a UIImageView which held the image of the back of the card. This let me simply toggle the alpha for that top view to display the card as either face up or face down. And then I added one more topMost view to the control - a UILabel with "background = .clear" and attached a UITapGestureRecognizer to it. When the control is tapped, this function which is meant to animate the flipping over of the card is called.
To construct the animation, I call the getCurrentFaceUp() function which temporarily sets the alpha of the card's faceDownView to 0 (so I can take a snapshot of the card underneath it as it is currently configured). It returns a UIImage of the "face up" view of the card. I already have a UIImage of the faceDown view. These are the 2 images I need for the transition.
So...then I set the background color of that topMost UILabel to .black, create a new temporary UIImageView and place it on top of the existing control. I set the temporary imageView to initially display whichever one of the 2 images is currently visible on the control. And then I run the flip transition, change the configuration of the background control to match the new state, change the label background back to .clear and dispose of the temporary UIImageView.
(If there's a better way to accomplish this, I'm open to hearing it but the main purpose of this post is to understand why my code appears to be acting strangely.)
When I was looking for a way to animate a card flip, I found a YouTube video that demonstrated the UIView.transition() with the flip animation. It did not require using a UIView.animate() wrapper in order to make it work - so I'm pretty certain that it's me that did something wrong -but I've spent hours testing variations and searching for someone else with this problem and I haven't been able to find the answer.
Thanks very much in advance to anybody that can help me understand what's going on here...
Upvotes: 0
Views: 720
Reputation: 77433
A little tough to tell (didn't try to actually run your code), but I think you may be doing more than you need to.
Take a look at this...
We'll start with two "Card View" subclasses - Front and Back (we'll "style" them in the next step):
class CardFrontView: UIView {
}
class CardBackView: UIView {
}
Then, a "Playing Card View" class, that contains a "Front" view (cyan) and a "Back" view (red). On init, we add the subviews and set the "Front" view hidden. On tap, we'll run the flip transition between the Front and Back views:
class PlayingCardView: UIView {
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .cyan
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
@objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and then here's a simple Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
}
}
The result:
So, next step, we'll add a little styling to the Front and Back views -- no changes to the PlayingCardView
functionality... just a couple new lines to set the styling...
Card Front View - with rounded corners, a border and labels at the corners and center:
class CardFrontView: UIView {
var theLabels: [UILabel] = []
var cardID: Int = 0 {
didSet {
theLabels.forEach {
$0.text = "\(cardID)"
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
for i in 1...5 {
let v = UILabel()
v.font = .systemFont(ofSize: 24.0)
v.translatesAutoresizingMaskIntoConstraints = false
addSubview(v)
switch i {
case 1:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 2:
v.topAnchor.constraint(equalTo: topAnchor, constant: 10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
case 3:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 16.0).isActive = true
case 4:
v.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -10.0).isActive = true
v.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -16.0).isActive = true
default:
v.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
v.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
}
theLabels.append(v)
}
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
}
}
Looks like this:
Card Back View - with rounded corners, a border and a cross-hatch pattern:
class CardBackView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
layer.cornerRadius = 6
// border
layer.borderWidth = 1.0
layer.borderColor = UIColor.gray.cgColor
layer.masksToBounds = true
}
override func layoutSubviews() {
super.layoutSubviews()
// simple cross-hatch pattern
let hReplicatorLayer = CAReplicatorLayer()
let vReplicatorLayer = CAReplicatorLayer()
let line = CAShapeLayer()
let pth = UIBezierPath()
pth.move(to: CGPoint(x: 0.0, y: 0.0))
pth.addLine(to: CGPoint(x: 20.0, y: 20.0))
pth.move(to: CGPoint(x: 20.0, y: 0.0))
pth.addLine(to: CGPoint(x: 0.0, y: 20.0))
line.strokeColor = UIColor.yellow.cgColor
line.lineWidth = 1
line.path = pth.cgPath
var instanceCount = Int((bounds.maxX + 0.0) / 20.0)
hReplicatorLayer.instanceCount = instanceCount
hReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(20, 0, 0)
instanceCount = Int((bounds.maxY + 0.0) / 20.0)
vReplicatorLayer.instanceCount = instanceCount
vReplicatorLayer.instanceTransform = CATransform3DMakeTranslation(0, 20, 0)
hReplicatorLayer.addSublayer(line)
vReplicatorLayer.addSublayer(hReplicatorLayer)
layer.addSublayer(vReplicatorLayer)
}
}
Looks like this:
Playing Card View:
class PlayingCardView: UIView {
var cardID: Int = 0 { didSet { cardFront.cardID = cardID } }
let cardFront: CardFrontView = CardFrontView()
let cardBack: CardBackView = CardBackView()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
// add both card views
// constraining all 4 sides to self
[cardFront, cardBack].forEach { v in
addSubview(v)
v.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
v.topAnchor.constraint(equalTo: topAnchor),
v.leadingAnchor.constraint(equalTo: leadingAnchor),
v.trailingAnchor.constraint(equalTo: trailingAnchor),
v.bottomAnchor.constraint(equalTo: bottomAnchor),
])
}
cardFront.backgroundColor = .white
cardBack.backgroundColor = .red
// start with cardFront hidden
cardFront.isHidden = true
// add a tap recognizer
let t = UITapGestureRecognizer(target: self, action: #selector(flipMe))
addGestureRecognizer(t)
}
@objc func flipMe() -> Void {
// fromView is the one that is NOT hidden
let fromView = cardBack.isHidden ? cardFront : cardBack
// toView is the one that IS hidden
let toView = cardBack.isHidden ? cardBack : cardFront
// if we're going from back-to-front
// flip from left
// else
// flip from right
let direction: UIView.AnimationOptions = cardBack.isHidden ? .transitionFlipFromRight : .transitionFlipFromLeft
UIView.transition(from: fromView,
to: toView,
duration: 0.5,
options: [direction, .showHideTransitionViews],
completion: { b in
// if we want to do something on completion
})
}
}
and finally, the same Controller example:
class FlipCardVC: UIViewController {
let pCard: PlayingCardView = PlayingCardView()
override func viewDidLoad() {
super.viewDidLoad()
let g = view.safeAreaLayoutGuide
pCard.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(pCard)
NSLayoutConstraint.activate([
pCard.centerXAnchor.constraint(equalTo: g.centerXAnchor),
pCard.centerYAnchor.constraint(equalTo: g.centerYAnchor),
pCard.widthAnchor.constraint(equalToConstant: 200.0),
pCard.heightAnchor.constraint(equalTo: pCard.widthAnchor, multiplier: 1.5),
])
pCard.cardID = 5
}
}
and here's the new result:
Upvotes: 1