Swift 默认的 AlertViewController 打破约束
Swift default AlertViewController breaking constraints
我正在尝试使用样式为 .actionSheet 的默认 AlertViewController。出于某种原因,警报会导致 约束错误 。只要不是通过按钮触发(显示)alertController,整个视图就不会出现约束错误。难道这是Xcode的错误?
我得到的确切错误如下所示:
2019-04-12 15:33:29.584076+0200 Appname[4688:39368] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x6000025a1e50 UIView:0x7f88fcf6ce60.width == - 16 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x6000025a1e50 UIView:0x7f88fcf6ce60.width == - 16 (active)>
这是我使用的代码:
@objc func changeProfileImageTapped(){
print("ChangeProfileImageButton tapped!")
let alert = UIAlertController(title: "Change your profile image", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Online Stock Library", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.view.tintColor = ColorCodes.logoPrimaryColor
self.present(alert, animated: true)
}
如您所见,它非常基础。这就是为什么我对我得到的奇怪行为感到非常困惑,因为这个 默认实现 应该不会导致任何错误,对吗?
不过,通过打破限制,警报可以在所有屏幕尺寸上正确显示,我将非常感谢我得到的任何帮助。
这个错误并不严重,似乎是 Apple 未修复的错误。此约束在呈现后立即以动画样式出现。 我试图在呈现之前捕获并更改它(更改值、关系、优先级)——因为这个动态添加的约束没有成功。
当您在 self.present(alert, animated: false)
中关闭动画并使用 alert.view.addSubview(UIView())
时 – 错误消失。我无法解释,但它确实有效!
let alert = UIAlertController(title: "Change your profile image", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Online Stock Library", style: .default, handler: nil))
let cancel = UIAlertAction(title: "Cancel", style: .destructive, handler: nil)
alert.addAction(cancel)
alert.view.addSubview(UIView()) // I can't explain it, but it works!
self.present(alert, animated: false)
以下删除警告而不需要禁用动画。假设 Apple 最终修复了警告的根本原因,它应该不会破坏其他任何东西。
extension UIAlertController {
func pruneNegativeWidthConstraints() {
for subView in self.view.subviews {
for constraint in subView.constraints where constraint.debugDescription.contains("width == - 16") {
subView.removeConstraint(constraint)
}
}
}
}
然后可以这样使用:
// After all addActions(...), just before calling present(...)
alertController.pruneNegativeWidthConstraints()
这是 iOS 版本中的一个新错误:
- 12.2
- 12.3
- 12.4
- 13.0
- 13.1
- 13.2
- 13.2.3
- 13.3
- 13.4
- 13.4.1
- 13.5
- 13.6
- 14.0
- 14.2
- 14.4
我们唯一能做的就是向 Apple 提交错误报告(我刚刚做了,你也应该这样做)。
iOS 的新版本发布后,我会尝试更新答案。
添加到 答案...这似乎解决了我的问题,并且不需要对现有代码进行任何更改。
extension UIAlertController {
override open func viewDidLoad() {
super.viewDidLoad()
pruneNegativeWidthConstraints()
}
func pruneNegativeWidthConstraints() {
for subView in self.view.subviews {
for constraint in subView.constraints where constraint.debugDescription.contains("width == - 16") {
subView.removeConstraint(constraint)
}
}
}
}
Objective-C的解决方案:
- 从 UIAlertController 继承你自己的 Alert Controller
像之前的回复一样定义剪枝函数
@implementation TemplateAlertController
-(void) viewDidLoad {
[super viewDidLoad];
[self mPruneNegativeWithConstraints];
}
-(void) mPruneNegativeWithConstraints {
for (UIView* iSubview in [self.view subviews]) {
for (NSLayoutConstraint* iConstraint in [iSubview constraints]) {
if ([iConstraint.debugDescription containsString:@"width == - 16"]) {
[iSubview removeConstraint:iConstraint];
}
}
}
}
@end
安全解决方案
您不应删除该约束,因为它在将来会以正确的值使用。
作为替代方案,您可以将其常量更改为正值:
class PXAlertController: UIAlertController {
override func viewDidLoad() {
super.viewDidLoad()
for subview in self.view.subviews {
for constraint in subview.constraints {
if constraint.firstAttribute == .width && constraint.constant == -16 {
constraint.constant = 10 // Any positive value
}
}
}
}
}
然后初始化你的控制器使用:
let controller = PXAlertController(title: "Title", message: "Message", preferredStyle: .actionSheet)
这里有有趣的想法。我个人不喜欢删除约束或更改其值(大小)的想法。
由于问题取决于约束解决方案被迫进入必须打破强制(优先级 1000)约束的位置,因此一种不那么残酷的方法只是告诉框架如果需要可以打破此约束。
所以(基于 Josh 的“Safe”class):
class PXAlertController: UIAlertController {
override func viewDidLoad() {
super.viewDidLoad()
tweakProblemWidthConstraints()
}
func tweakProblemWidthConstraints() {
for subView in self.view.subviews {
for constraint in subView.constraints {
// Identify the problem constraint
// Check that it's priority 1000 - which is the cause of the conflict.
if constraint.firstAttribute == .width &&
constraint.constant == -16 &&
constraint.priority.rawValue == 1000 {
// Let the framework know it's okay to break this constraint
constraint.priority = UILayoutPriority(rawValue: 999)
}
}
}
}
}
这样做的好处是它不会改变任何布局尺寸,而且在框架修复的情况下也很有可能表现良好。
在 iPhone SE 模拟器中测试(这是我原来的问题)- 与约束相关的调试已经消失。
另一种避免 NSLayoutConstraint 错误的方法是使用 preferredStyle: .alert
而不是 preferredStyle: .actionSheet
。这可以在不产生警告的情况下工作,但它会以模态方式显示菜单。
如果你想保留动画和所有约束,你应该在呈现警报控制器之前找到一个负约束并将其设为正约束。
// Find negative constraint and make it positive
for subview in alert.view.subviews {
for constraint in subview.constraints {
if constraint.constant < 0 {
constraint.constant = -constraint.constant
}
}
}
// Present alert controller
present(alert, animated: true)
这是我用来解决问题的功能。出现这个问题是因为约束是负的,我不知道为什么。
func showActionSheet(title: String, message: String, actions: [UIAlertAction]) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
actions.forEach { alertController.addAction([=10=]) }
let subviewConstraint = alertController.view.subviews
.flatMap({ [=10=].constraints })
.filter({ [=10=].constant < 0 })
for subviewConstraint in subviewConstraint {
subviewConstraint.constant = -subviewConstraint.constant // this is the answer
}
self.present(alertController, animated: true)
}
创建视图扩展以获取所有约束
extension UIView {
func callRecursively(_ body: (_ subview: UIView) -> Void) {
body(self)
subviews.forEach { [=10=].callRecursively(body) }
}
}
创建 UIAlertController 扩展以查找所有具有 -16 常量的约束并将其优先级更改为 999
extension UIAlertController {
func fixConstraints() -> UIAlertController {
view.callRecursively { subview in
subview.constraints
.filter({ [=11=].constant == -16 })
.forEach({ [=11=].priority = UILayoutPriority(rawValue: 999)})
}
return self
}
}
创建提醒并在呈现时调用 fixConstraints():
let alert = UIAlertController(...
...
present(alert.fixConstraints(), animated: true, completion: nil)
各位,我想我明白了。问题是,当 popoverPresentationController sourceView 被分配给 UIAlertController 的 self.view 时,会发生循环引用,并且约束会中断。应为 sourceView 分配调用弹出窗口的视图,而不是弹出窗口本身。
我正在尝试使用样式为 .actionSheet 的默认 AlertViewController。出于某种原因,警报会导致 约束错误 。只要不是通过按钮触发(显示)alertController,整个视图就不会出现约束错误。难道这是Xcode的错误?
我得到的确切错误如下所示:
2019-04-12 15:33:29.584076+0200 Appname[4688:39368] [LayoutConstraints] Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(
"<NSLayoutConstraint:0x6000025a1e50 UIView:0x7f88fcf6ce60.width == - 16 (active)>"
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0x6000025a1e50 UIView:0x7f88fcf6ce60.width == - 16 (active)>
这是我使用的代码:
@objc func changeProfileImageTapped(){
print("ChangeProfileImageButton tapped!")
let alert = UIAlertController(title: "Change your profile image", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Online Stock Library", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
alert.view.tintColor = ColorCodes.logoPrimaryColor
self.present(alert, animated: true)
}
如您所见,它非常基础。这就是为什么我对我得到的奇怪行为感到非常困惑,因为这个 默认实现 应该不会导致任何错误,对吗?
不过,通过打破限制,警报可以在所有屏幕尺寸上正确显示,我将非常感谢我得到的任何帮助。
这个错误并不严重,似乎是 Apple 未修复的错误。此约束在呈现后立即以动画样式出现。
当您在 self.present(alert, animated: false)
中关闭动画并使用 alert.view.addSubview(UIView())
时 – 错误消失。我无法解释,但它确实有效!
let alert = UIAlertController(title: "Change your profile image", message: nil, preferredStyle: .actionSheet)
alert.addAction(UIAlertAction(title: "Photo Library", style: .default, handler: nil))
alert.addAction(UIAlertAction(title: "Online Stock Library", style: .default, handler: nil))
let cancel = UIAlertAction(title: "Cancel", style: .destructive, handler: nil)
alert.addAction(cancel)
alert.view.addSubview(UIView()) // I can't explain it, but it works!
self.present(alert, animated: false)
以下删除警告而不需要禁用动画。假设 Apple 最终修复了警告的根本原因,它应该不会破坏其他任何东西。
extension UIAlertController {
func pruneNegativeWidthConstraints() {
for subView in self.view.subviews {
for constraint in subView.constraints where constraint.debugDescription.contains("width == - 16") {
subView.removeConstraint(constraint)
}
}
}
}
然后可以这样使用:
// After all addActions(...), just before calling present(...)
alertController.pruneNegativeWidthConstraints()
这是 iOS 版本中的一个新错误:
- 12.2
- 12.3
- 12.4
- 13.0
- 13.1
- 13.2
- 13.2.3
- 13.3
- 13.4
- 13.4.1
- 13.5
- 13.6
- 14.0
- 14.2
- 14.4
我们唯一能做的就是向 Apple 提交错误报告(我刚刚做了,你也应该这样做)。
iOS 的新版本发布后,我会尝试更新答案。
添加到
extension UIAlertController {
override open func viewDidLoad() {
super.viewDidLoad()
pruneNegativeWidthConstraints()
}
func pruneNegativeWidthConstraints() {
for subView in self.view.subviews {
for constraint in subView.constraints where constraint.debugDescription.contains("width == - 16") {
subView.removeConstraint(constraint)
}
}
}
}
Objective-C的解决方案:
- 从 UIAlertController 继承你自己的 Alert Controller
像之前的回复一样定义剪枝函数
@implementation TemplateAlertController -(void) viewDidLoad { [super viewDidLoad]; [self mPruneNegativeWithConstraints]; } -(void) mPruneNegativeWithConstraints { for (UIView* iSubview in [self.view subviews]) { for (NSLayoutConstraint* iConstraint in [iSubview constraints]) { if ([iConstraint.debugDescription containsString:@"width == - 16"]) { [iSubview removeConstraint:iConstraint]; } } } } @end
安全解决方案
您不应删除该约束,因为它在将来会以正确的值使用。
作为替代方案,您可以将其常量更改为正值:
class PXAlertController: UIAlertController {
override func viewDidLoad() {
super.viewDidLoad()
for subview in self.view.subviews {
for constraint in subview.constraints {
if constraint.firstAttribute == .width && constraint.constant == -16 {
constraint.constant = 10 // Any positive value
}
}
}
}
}
然后初始化你的控制器使用:
let controller = PXAlertController(title: "Title", message: "Message", preferredStyle: .actionSheet)
这里有有趣的想法。我个人不喜欢删除约束或更改其值(大小)的想法。
由于问题取决于约束解决方案被迫进入必须打破强制(优先级 1000)约束的位置,因此一种不那么残酷的方法只是告诉框架如果需要可以打破此约束。
所以(基于 Josh 的“Safe”class):
class PXAlertController: UIAlertController {
override func viewDidLoad() {
super.viewDidLoad()
tweakProblemWidthConstraints()
}
func tweakProblemWidthConstraints() {
for subView in self.view.subviews {
for constraint in subView.constraints {
// Identify the problem constraint
// Check that it's priority 1000 - which is the cause of the conflict.
if constraint.firstAttribute == .width &&
constraint.constant == -16 &&
constraint.priority.rawValue == 1000 {
// Let the framework know it's okay to break this constraint
constraint.priority = UILayoutPriority(rawValue: 999)
}
}
}
}
}
这样做的好处是它不会改变任何布局尺寸,而且在框架修复的情况下也很有可能表现良好。
在 iPhone SE 模拟器中测试(这是我原来的问题)- 与约束相关的调试已经消失。
另一种避免 NSLayoutConstraint 错误的方法是使用 preferredStyle: .alert
而不是 preferredStyle: .actionSheet
。这可以在不产生警告的情况下工作,但它会以模态方式显示菜单。
如果你想保留动画和所有约束,你应该在呈现警报控制器之前找到一个负约束并将其设为正约束。
// Find negative constraint and make it positive
for subview in alert.view.subviews {
for constraint in subview.constraints {
if constraint.constant < 0 {
constraint.constant = -constraint.constant
}
}
}
// Present alert controller
present(alert, animated: true)
这是我用来解决问题的功能。出现这个问题是因为约束是负的,我不知道为什么。
func showActionSheet(title: String, message: String, actions: [UIAlertAction]) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
actions.forEach { alertController.addAction([=10=]) }
let subviewConstraint = alertController.view.subviews
.flatMap({ [=10=].constraints })
.filter({ [=10=].constant < 0 })
for subviewConstraint in subviewConstraint {
subviewConstraint.constant = -subviewConstraint.constant // this is the answer
}
self.present(alertController, animated: true)
}
创建视图扩展以获取所有约束
extension UIView {
func callRecursively(_ body: (_ subview: UIView) -> Void) {
body(self)
subviews.forEach { [=10=].callRecursively(body) }
}
}
创建 UIAlertController 扩展以查找所有具有 -16 常量的约束并将其优先级更改为 999
extension UIAlertController {
func fixConstraints() -> UIAlertController {
view.callRecursively { subview in
subview.constraints
.filter({ [=11=].constant == -16 })
.forEach({ [=11=].priority = UILayoutPriority(rawValue: 999)})
}
return self
}
}
创建提醒并在呈现时调用 fixConstraints():
let alert = UIAlertController(...
...
present(alert.fixConstraints(), animated: true, completion: nil)
各位,我想我明白了。问题是,当 popoverPresentationController sourceView 被分配给 UIAlertController 的 self.view 时,会发生循环引用,并且约束会中断。应为 sourceView 分配调用弹出窗口的视图,而不是弹出窗口本身。