Swift默认的AlertViewController破坏约束问题

90

我正在尝试使用样式.actionSheet的默认AlertViewController。出于某种原因,警报会导致约束错误。只要通过按钮没有触发alertController(被显示),整个视图上就没有约束错误。这可能是Xcode的bug吗?

我收到的确切错误看起来像这样:

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)
    }

正如您所看到的,这是非常基础的。这就是为什么我对我得到的奇怪行为感到非常困惑,因为这个默认实现不应该引起任何错误,对吧?

Output I get

虽然通过打破限制,警报在所有屏幕尺寸上都能正常显示,但我会非常感谢任何帮助。


你在代码中构建了其他约束吗? - Shehata Gamal
1
@linus_hologram,这个东西会在屏幕上造成任何视觉故障吗?如果是__是__的话,那是什么?如果__不是__,那就不要浪费时间去修复根本没有问题的东西。 - holex
1
约束日志并不总是正确的,有时会产生误导性的信息。 - Shehata Gamal
这很可能是由于您的约束条件出现问题,而不是警报视图本身。如果您停用<NSLayoutConstraint:0x6000025a1e50 UIView:0x7f88fcf6ce60.width == - 16 (active)>,错误应该会消失,但可能会影响其他内容。 - Vanya
1
我使用调试器检查了这个问题,发现它是动态添加的约束... 在呈现UIAlertController之前我无法找到这个约束... 请参见下面我的回答获取更多信息。 - Agisight
显示剩余9条评论
12个回答

53
以下内容可以消除警告,而无需禁用动画。并且假设苹果最终修复了警告的根本原因,它不应该破坏其他任何东西。

以下方法可以解决警告问题,同时不需要禁用动画。而且如果苹果最终修复了这个警告的根本原因,这个方法也不会影响其他功能。

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()

1
非常好,不再有日志污染! - Klaas
这实际上现在(iOS 13.2.2)正在引起更多问题,不确定它在早期版本上是如何工作的。导致UIAlertController固定显示在屏幕顶部。 - jovanjovanovic
@jovanjovanovic 很抱歉听到你遇到了这个问题,但我无法重现你所描述的情况。这个解决方案仍然是可以消除警告而不会产生负面影响的。如果你有能够重现你所描述情况的代码示例,请分享一下。 - jims1103
我在创建警报后立即使用此代码,但似乎不起作用!最好将代码放在哪里? - Lucas Tegliabue
@LucasTegliabue 在呈现 AlertController 之前使用它,如下所示: let alertController = UIAlertController() alertController.pruneNegativeWidthConstraints() self.present(alertController, animated: true, completion: nil) - Jack
显示剩余2条评论

43
这个错误不是关键性的,似乎是Apple未修复的错误。此限制在呈现后的动画样式中出现。enter image description here我尝试在呈现之前捕获和更改它(更改值、关系、优先级),但由于这些动态添加的约束,没有成功。
当你在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)

1
这个答案与OP所遇到的约束问题有什么关系? - holex
4
关闭动画后,alert.view.addSubview(UIView()) 对我起了作用。但是没有动画,感觉很奇怪...无论如何,感谢这个解决方案。 - Tieda Wei
1
是的,我把它解释为苹果公司的错误。我已经解释过了。但如果你能够解决它,你可以尝试获取所需的限制并更改它。我认为这不是一个好主意,因为这是一个错误。 - Agisight
1
我认为添加子视图会强制重新计算自动布局约束,从而消除警告。 - FormigaNinja
3
animation设置为true时,就会出现这个问题。我认为这是一个内部错误,但由于ActionSheet能够正确显示,可以忽略它。我很高兴我能找到根本原因,一开始以为是我的代码的问题。 - Jerry Okafor
显示剩余6条评论

21

这是iOS版本中的一个新bug:

  • 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

我们唯一能做的就是向苹果提供一个bug报告(我刚刚已经这样做了,你也应该这么做)。

当新版本的iOS推出时,我会尽量更新答案。


2
看起来他们在iOS 14.5中修复了它 - 在使用iPhone SE2模拟器的Swift 5 / Xcode 12.5 / iOS 12.3项目中进行了测试。在同一项目中,iPhone SE 12.4模拟器会在控制台中记录该消息。 - Neph

8

这个答案的基础上,我的解决方法似乎可以解决这个问题,并且不需要对现有代码进行任何更改。

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)
            }
        }
    }
}

2

安全解决方案

您不应该移除限制条件,因为它将在未来与正确的值一起使用。

作为替代方案,您可以将其常量更改为正值:

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)

我尝试了所有适用于Swift的解决方案,但都没有成功。我正在使用XCode 11.5进行编译,针对iOS 13.0运行在iPhone Xs Max上。仍然出现"<NSLayoutConstraint:0x28002b930 UIView:0x102e32420.width == - 16 (active)>"。如果没有问题,我不能提供太多帮助。正如有人建议的那样,我将忽略这个错误,但我不喜欢它。 - Nicola Mingotti
它根本不起作用。RichW的解决方案至少在iOS 13上有效。 - Vyachaslav Gerchicov
@VyachaslavGerchicov 不过要小心,我认为在运行时将必需的约束条件更改为非必需的旧版iOS版本会崩溃。 https://dev59.com/jV0Z5IYBdhLWcg3wrR4r - Josh Bernfeld

1
另一种避免NSLayoutConstraint错误的方法是使用preferredStyle: .alert而不是preferredStyle: .actionSheet。这样可以避免产生警告,但它会以模态方式显示菜单。

1

这里有一些有趣的想法。就个人而言,我不喜欢删除约束或更改其值(大小)的想法。

由于问题取决于将约束分辨率强制定位到必须打破强制性(优先级1000)约束的位置,一个不那么残忍的方法只是告诉框架如果需要可以打破此约束。

因此(基于Josh的“Safe”类):

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 模拟器中测试(这是我最初的问题)- 与约束相关的调试已经消失了。

在iOS 13中可以工作,但在iOS 12中无法工作。已在iPhone X模拟器上尝试过。 - Vyachaslav Gerchicov
@RichW 这是一个不错的方法。但要小心,我认为旧版的iOS在运行时将必需的约束条件更改为非必需时会崩溃。https://dev59.com/jV0Z5IYBdhLWcg3wrR4r - Josh Bernfeld

0
创建视图扩展以获取所有约束。
extension UIView {
   func callRecursively(_ body: (_ subview: UIView) -> Void) {
      body(self)
      subviews.forEach { $0.callRecursively(body) }
   }
}

创建 UIAlertController 扩展,以查找所有常量为 -16 的约束并将其优先级更改为 999。
extension UIAlertController {
   func fixConstraints() -> UIAlertController {
      view.callRecursively { subview in
         subview.constraints
            .filter({ $0.constant == -16 })
            .forEach({ $0.priority = UILayoutPriority(rawValue: 999)})
    }
    return self
    }
}

创建你的警报并在呈现时调用 fixConstraints()

let alert = UIAlertController(...
...
present(alert.fixConstraints(), animated: true, completion: nil)

0
这是我用来解决问题的函数。出现问题的原因是约束被减去了,但我不知道为什么。
    func showActionSheet(title: String, message: String, actions: [UIAlertAction]) {
        let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
        actions.forEach { alertController.addAction($0) }
        let subviewConstraint = alertController.view.subviews
            .flatMap({ $0.constraints })
            .filter({ $0.constant < 0 })
        for subviewConstraint in subviewConstraint {
            subviewConstraint.constant = -subviewConstraint.constant // this is the answer
        }
        self.present(alertController, animated: true)
    }

0
大家好,我想我找到了问题所在。问题是当popoverPresentationController的sourceView被分配为UIAlertController的self.view时,会发生循环引用并且约束会破裂。应该将sourceView分配给调用弹出窗口的视图,而不是弹出窗口本身。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接