UIView弹窗旋转后竖屏拉伸异常:无法自适应至最大高度求助
我有一个UIView弹窗,内容可能垂直溢出,因此设置了高度约束(lessThanOrEqualToConstant)来限制弹窗高度不超过屏幕安全区的上下边距。预期效果是弹窗根据内容自动调整高度,直到达到最大可用高度。但现在遇到的问题是:设备旋转到横屏再转回竖屏后,弹窗无法垂直拉伸到竖屏的最大可用高度。
我已经尝试过以下方案,但都无效:
- 为高度约束设置高优先级
- 为
itemView设置高抗压缩优先级 - 更新约束后调用
layoutIfNeeded - 添加低优先级的高度约束强制视图拉伸
初始状态、旋转后、转回竖屏后的效果见对应截图。
相关代码
PopupView 实现
class PopupView: UIView, SpinnerProvider { lazy var spinner: SNSpinnerView = { let spinner = SNSpinnerView() spinner.translatesAutoresizingMaskIntoConstraints = false spinner.spinnerImageView.image = spinnerImageDefault spinner.isHidden = false return spinner }() var itemView = UIView() { didSet { spinner.isHidden = true UIView.animate(withDuration: Constants.animationDuration) { self.backgroundColor = .snDarkDimming } popupContainerView.addChild(itemView) } } override init(frame: CGRect) { super.init(frame: frame) setup() } private lazy var popupContainerView: UIView = { let view = UIView(frame: .zero) view.layer.cornerRadius = Constants.popUpFrameCornerRadius view.layer.masksToBounds = true view.accessibilityIdentifier = "popup" return view }() private lazy var containerHeightConstraint: NSLayoutConstraint = { // Constant will change. popupContainerView.heightAnchor.constraint(lessThanOrEqualToConstant: 0) }() private lazy var containerLeadingConstraint: NSLayoutConstraint = { popupContainerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor) }() private lazy var containerTrailingConstraint: NSLayoutConstraint = { popupContainerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor) }() required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setup() { backgroundColor = .snLoadingViewBackground useAutoLayout() addSubview(popupContainerView) popupContainerView.useAutoLayout() .center(in: self) addSubview(spinner) spinner.useAutoLayout() .center(in: self) spinner.startRotationAnimation() NSLayoutConstraint.activate([ containerHeightConstraint, containerLeadingConstraint, containerTrailingConstraint ]) adjustConstraints() } func adjustConstraints() { if UIDevice.current.orientation.isPortrait { containerLeadingConstraint.constant = Constants.Portrait.leadingInset containerTrailingConstraint.constant = Constants.Portrait.trailingInset containerHeightConstraint.constant = UIScreen.main.bounds.height - safeAreaLayoutGuide.layoutFrame.height - (Constants.Portrait.horizontalInset * 2) } else { containerLeadingConstraint.constant = Constants.Landscape.leadingInset containerTrailingConstraint.constant = Constants.Landscape.trailingInset containerHeightConstraint.constant = UIScreen.main.bounds.height - (Constants.Landscape.horizontalInset * 2) } } }
父视图中调用约束调整
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) popupView.adjustConstraints() }
问题分析与解决方案
问题根源在于三个核心错误:
高度约束逻辑错误
当前的containerHeightConstraint使用lessThanOrEqualToConstant,但计算的常量值逻辑错误——竖屏时UIScreen.main.bounds.height - safeAreaLayoutGuide.layoutFrame.height得到的是上下安全区总高度,再减去两倍边距会得到负数,导致约束完全失效。应该改用基于锚点的约束,直接绑定到安全区高度,避免手动计算的误差。itemView缺少约束
你将itemView添加到popupContainerView后未设置任何约束,导致容器无法感知内容高度,自然无法根据内容拉伸。必须让itemView填充容器,这样容器才能根据内容自动调整高度。旋转时约束更新时机错误
在viewWillTransition中直接调用adjustConstraints时,设备Orientation尚未完成更新,此时获取的UIDevice.current.orientation是旧值,导致约束计算错误。应该利用过渡协调器的动画块,并通过传入的size参数判断横竖屏。
修正后的 PopupView 代码
class PopupView: UIView, SpinnerProvider { lazy var spinner: SNSpinnerView = { let spinner = SNSpinnerView() spinner.translatesAutoresizingMaskIntoConstraints = false spinner.spinnerImageView.image = spinnerImageDefault spinner.isHidden = false return spinner }() var itemView = UIView() { didSet { spinner.isHidden = true UIView.animate(withDuration: Constants.animationDuration) { self.backgroundColor = .snDarkDimming } // 为itemView添加填充容器的约束 popupContainerView.addSubview(itemView) itemView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ itemView.topAnchor.constraint(equalTo: popupContainerView.topAnchor), itemView.leadingAnchor.constraint(equalTo: popupContainerView.leadingAnchor), itemView.trailingAnchor.constraint(equalTo: popupContainerView.trailingAnchor), itemView.bottomAnchor.constraint(equalTo: popupContainerView.bottomAnchor) ]) } } override init(frame: CGRect) { super.init(frame: frame) setup() } private lazy var popupContainerView: UIView = { let view = UIView(frame: .zero) view.layer.cornerRadius = Constants.popUpFrameCornerRadius view.layer.masksToBounds = true view.accessibilityIdentifier = "popup" return view }() // 改用基于安全区锚点的高度约束,避免手动计算常量 private lazy var containerHeightConstraint: NSLayoutConstraint = { popupContainerView.heightAnchor.constraint(lessThanOrEqualTo: safeAreaLayoutGuide.heightAnchor, constant: -(Constants.Portrait.horizontalInset * 2)) }() private lazy var containerLeadingConstraint: NSLayoutConstraint = { popupContainerView.leadingAnchor.constraint(equalTo: safeAreaLayoutGuide.leadingAnchor, constant: Constants.Portrait.leadingInset) }() private lazy var containerTrailingConstraint: NSLayoutConstraint = { popupContainerView.trailingAnchor.constraint(equalTo: safeAreaLayoutGuide.trailingAnchor, constant: -Constants.Portrait.trailingInset) }() // 绑定到安全区垂直居中,确保弹窗在可用空间内居中 private lazy var containerCenterYConstraint: NSLayoutConstraint = { popupContainerView.centerYAnchor.constraint(equalTo: safeAreaLayoutGuide.centerYAnchor) }() required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } private func setup() { backgroundColor = .snLoadingViewBackground translatesAutoresizingMaskIntoConstraints = false addSubview(popupContainerView) popupContainerView.translatesAutoresizingMaskIntoConstraints = false addSubview(spinner) spinner.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ spinner.centerXAnchor.constraint(equalTo: centerXAnchor), spinner.centerYAnchor.constraint(equalTo: centerYAnchor) ]) spinner.startRotationAnimation() NSLayoutConstraint.activate([ containerHeightConstraint, containerLeadingConstraint, containerTrailingConstraint, containerCenterYConstraint ]) adjustConstraints(for: bounds.size) } // 改用size参数判断横竖屏,避免依赖未更新的Orientation func adjustConstraints(for size: CGSize) { let isPortrait = size.height >= size.width if isPortrait { containerLeadingConstraint.constant = Constants.Portrait.leadingInset containerTrailingConstraint.constant = -Constants.Portrait.trailingInset containerHeightConstraint.constant = -(Constants.Portrait.horizontalInset * 2) } else { containerLeadingConstraint.constant = Constants.Landscape.leadingInset containerTrailingConstraint.constant = -Constants.Landscape.trailingInset containerHeightConstraint.constant = -(Constants.Landscape.horizontalInset * 2) } layoutIfNeeded() } }
修正后的父视图调用代码
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) // 在过渡动画块中更新约束,确保时机正确 coordinator.animate(alongsideTransition: { _ in self.popupView.adjustConstraints(for: size) }, completion: nil) }
内容的提问来源于stack exchange,提问作者Avi

