iOS从推送页面返回时viewWillAppear中按钮标签初始透明问题
问题描述
我有一个包含2个图标和UILabel的自定义UIButton,在.selected状态下有不同背景色。点击按钮后切换为选中状态并推送新ViewController,返回时想模仿UITableViewCell的选中过渡动画,用UIView.transition(with: button, duration: 0.3, options: .transitionCrossDissolve) { button.isSelected = false }实现背景色过渡。这段代码放在viewDidAppear中功能正常但时机偏晚,移到viewWillAppear后,按钮上的UILabel初始为透明,只有标签会从透明动画到正常颜色,图标显示正常。
演示代码
import UIKit class ViewController: UIViewController { var myButton: MyButton? override func viewDidLoad() { super.viewDidLoad() myButton = MyButton() view.addSubview(myButton!) myButton?.frame.origin.x = 200 myButton?.frame.origin.y = 200 myButton?.addAction(.init { [weak self] _ in self?.myButton?.isSelected = true self?.navigationController?.pushViewController(ViewController2(), animated: true) }, for: .touchUpInside) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) UIView.transition(with: myButton!, duration: 3, options: .transitionCrossDissolve) { self.myButton!.isSelected = false } } // override func viewDidAppear(_ animated: Bool) { // super.viewDidAppear(animated) // UIView.transition(with: myButton!, duration: 3, options: .transitionCrossDissolve) { self.myButton!.isSelected = false } // } } class ViewController2: UIViewController { override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white } } class MyButton: UIButton { let label = UILabel("Hahahaha", font: .systemFont(ofSize: 14), textColor: .black) convenience init() { self.init(frame: .init(x: 0, y: 0, width: 200, height: 200)) setBackgroundColor(color: .white, forState: .normal) setBackgroundColor(color: .gray, forState: .highlighted) setBackgroundColor(color: .gray, forState: .selected) addSubview(label) label.frame.origin = .init(x: 40, y: 40) } } extension UIButton { func setBackgroundColor(color: UIColor, forState: UIControl.State) { self.clipsToBounds = true UIGraphicsBeginImageContext(CGSize(width: 1, height: 1)) if let context = UIGraphicsGetCurrentContext() { context.setFillColor(color.cgColor) context.fill(CGRect(x: 0, y: 0, width: 1, height: 1)) let colorImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() self.setBackgroundImage(colorImage, for: forState) } } } extension UILabel { convenience init(_ text: String = "", font: UIFont? = nil, textColor: UIColor? = .black) { self.init() self.text = text self.font = font sizeToFit() self.textColor = textColor } }
原因分析
viewWillAppear阶段,视图控制器的视图还未完成完全的布局和渲染,UILabel的初始绘制状态可能还未被系统确认。- 使用
.transitionCrossDissolve过渡时,系统会对目标视图(整个UIButton)的当前状态和最终状态做快照对比。在viewWillAppear时,UILabel尚未完成首次渲染,快照中它的状态是透明的,导致动画过程中出现从透明到正常颜色的过渡。 viewDidAppear时视图已经完成渲染,快照能正确捕获UILabel的正常状态,所以动画表现正常。
解决方法
方法1:强制UILabel提前完成渲染
在自定义MyButton的初始化中,添加代码让label提前完成布局和绘制:
convenience init() { self.init(frame: .init(x: 0, y: 0, width: 200, height: 200)) // 原有背景色设置代码 addSubview(label) label.frame.origin = .init(x: 40, y: 40) // 强制label完成布局与绘制 label.layoutIfNeeded() label.layer.displayIfNeeded() }
方法2:延迟动画到视图渲染完成后执行
在viewWillAppear中用DispatchQueue.main.async包裹动画代码,让动画在当前runloop结束后执行,此时视图已完成初始渲染:
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) DispatchQueue.main.async { UIView.transition(with: self.myButton!, duration: 3, options: .transitionCrossDissolve) { self.myButton!.isSelected = false } } }
方法3:改用属性动画替代整体视图过渡
避免对整个按钮做过渡,直接对按钮状态切换做属性动画,依赖UIButton状态变化时的属性响应:
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) UIView.animate(withDuration: 0.3) { self.myButton?.isSelected = false } }
当前的setBackgroundColor方法已经通过背景图片实现状态切换,属性动画会自动处理背景色的平滑过渡。
内容的提问来源于stack exchange,提问作者Siamaster
相关产品推荐
相关产品推荐

