问题描述
我正在使用 UIView.transition 翻转卡片。
虽然故障排除它不起作用,但我偶然发现了一种使它起作用的方法 - 但我不知道为什么。我希望有人可以查看下面的两个代码块并帮助我理解为什么一个有效而另一个无效。我觉得很奇怪。
首先,这是实际工作的代码块。卡片翻转在视觉上完美无瑕。
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
}
}
}
但我不明白为什么我似乎需要将 UIView.transition() 包装到调用 UIView.animate() 的完成处理程序中,以便翻转动画工作。
*(注意:如果我从 animate() 块中拉出“imageView.alpha = 1.0”并在调用 UIView.animate() 之前立即放置它 - 翻转动画不会不发生(带有或没有 layoutIfNeeded() 调用。它只是切换图像。*
现在,这是我期望工作的代码 - 但是当我使用此代码而不是上面的代码时,没有“翻转”转换。卡片图像会立即在正面朝上和正面朝下图像之间切换。此处的“UIView.transition”调用与上述代码中的调用相同。这里唯一的区别是它NOT被包裹在一个 0.01 秒的 UIView.animate 完成块中。
imageView.alpha = 1.0
imageView.layoutIfNeeded()
UIView.transition(with: imageView,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
}
}
这个转换结束了我的 flipTicket() 函数。在这两种情况下,此转换之前的代码是相同的。我不打算包括它,因为我认为没有必要理解这个问题——但话说回来——我知道什么?以下是上述剪辑之前的内容:
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()
背景: 此代码是代表扑克牌的自定义 UI 控件的一部分。它由几个子视图组成。最上面的子视图最初是一个 UIImageView,它保存了卡片背面的图像。这让我可以简单地切换该顶视图的 alpha 以将卡片显示为面朝上或面朝下。然后我向控件添加了一个 topMost 视图 - 一个带有“background = .clear”的 UILabel 并附加了一个 UITapGestureRecognizer 到它。当点击控件时,将调用此函数,该函数旨在为卡片的翻转设置动画。
为了构建动画,我调用了 getCurrentFaceUp() 函数,该函数临时将卡片的 faceDownView 的 alpha 设置为 0(因此我可以根据当前配置对卡片下方的卡片进行快照)。它返回卡片“面朝上”视图的 UIImage。我已经有了 faceDown 视图的 UIImage。这些是过渡所需的 2 张图片。
所以...然后我将 topMost UILabel 的背景颜色设置为 .black,创建一个新的临时 UIImageView 并将其放在现有控件的顶部。我将临时 imageView 设置为最初显示控件上当前可见的 2 个图像中的任何一个。然后我运行翻转过渡,更改背景控件的配置以匹配新状态,将标签背景更改回 .clear 并处理临时 UIImageView。
(如果有更好的方法来实现这一点,我愿意倾听,但这篇文章的主要目的是了解为什么我的代码看起来很奇怪。)
当我在寻找一种为卡片翻转设置动画的方法时,我发现了一个 YouTube 视频,该视频演示了 UIView.transition() 与翻转动画。它不需要使用 UIView.animate() 包装器来使其工作 - 所以我很确定是我做错了 - 但我花了几个小时测试变体并寻找其他有这个问题的人,我一直没能找到答案。
非常感谢任何可以帮助我了解这里发生的事情的人......
解决方法
有点难说(没有尝试实际运行您的代码),但我认为您可能做的比需要的多。
看看这个...
我们将从两个“卡片视图”子类开始 - Front 和 Back(我们将在下一步中对它们进行“样式化”):
class CardFrontView: UIView {
}
class CardBackView: UIView {
}
然后是一个“扑克牌视图”类,它包含一个“前”视图(青色)和一个“后”视图(红色)。在初始化时,我们添加子视图并将“前”视图设置为隐藏。点击后,我们将在 Front 和 Back 视图之间运行翻转过渡:
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
})
}
}
然后是一个简单的控制器示例:
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),])
}
}
结果:
所以,下一步,我们将向 Front 和 Back 视图添加一些样式 -- 没有更改 PlayingCardView
功能...只是几行新行来设置样式...>
卡片正面视图 - 带有圆角、边框和边角和中心的标签:
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
}
}
看起来像这样:
卡片背面视图 - 带有圆角、边框和交叉影线图案:
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)
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)
}
}
看起来像这样:
扑克牌视图 - 唯一的变化是将前卡背景颜色设置为白色,并将其“ID”设置为 5:
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,])
}
cardFront.backgroundColor = .white
cardFront.cardID = 5
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,completion: { b in
// if we want to do something on completion
})
}
}
最后,相同的控制器示例:
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),])
}
}
这是新的结果: