Jim
Jim

Reputation: 75

Why does my UIView.Transition not work unless wrapped in a UIView.Animate block?

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

Answers (1)

DonMag
DonMag

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:

enter image description here

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:

enter image description here

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:

enter image description here

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:

enter image description here

Upvotes: 1

Related Questions