'Swift default AlertViewController breaking constraints
I am trying to use a default AlertViewController with style .actionSheet. For some reason, the alert causes a constraint error. As long as the alertController is not triggered (displayed) through a button, there are no constraint errors on the whole view. Could it be that this is a bug of Xcode?
The exact error I get looks like this:
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)>
This is the code I use:
@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)
}
As you can see, it is very basic. That's why I am very confused about the strange behavior I get as this default implementation should not cause any errors, right?
Although, through breaking the constraints, the alert displays properly on all screen sizes I would be really thankful for any help I get.
Solution 1:[1]
The following removes the warning without needing to disable animation. And assuming Apple eventually fixes the root cause of the warning, it shouldn't break anything else.
extension UIAlertController {
func pruneNegativeWidthConstraints() {
for subView in self.view.subviews {
for constraint in subView.constraints where constraint.debugDescription.contains("width == - 16") {
subView.removeConstraint(constraint)
}
}
}
}
This can then be used like this:
// After all addActions(...), just before calling present(...)
alertController.pruneNegativeWidthConstraints()
Solution 2:[2]
It's a new bug in iOS versions:
- 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
The only thing we can do is to file a bug report to Apple (I just did that and you should too).
I'll try to update answer for a new version(s) of iOS when it come out.
Solution 3:[3]
Adding to this answer...This seems to remove the issue for me and doesn't require any changes to existing code.
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)
}
}
}
}
Solution 4:[4]
Safe Solution
You should not remove the constraint because it is used in the future with a correct value.
As an alternative, you can change its constant to a positive value:
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
}
}
}
}
}
And then to initialize your controller use:
let controller = PXAlertController(title: "Title", message: "Message", preferredStyle: .actionSheet)
Solution 5:[5]
Interesting ideas here. Personally I don't like the idea of deleting the constraint or changing it's value (size).
As the issue hinges on the constraint resolution being forced into a position where it must break a mandated (priority 1000) constraint, a less brutal approach is just to tell the framework that this constraint could be broken if needed.
So (based on Josh's "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)
}
}
}
}
}
This has the advantages that it doesn't change any layout dimensions, it also stands a good chance of being well behaved in the event of a fix in the framework.
Tested in iPhone SE simulator (which was giving me my original problem) - constraint related debug has gone.
Solution 6:[6]
An alternative way of getting away from the NSLayoutConstraint bug, is to use preferredStyle: .alert instead of preferredStyle: .actionSheet. This works without generating warnings, but it will display the menu modally.
Solution 7:[7]
The solution for Objective-C:
- Subclass your own Alert Controller from UIAlertController
Define prune-function like in previous reply
@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
Solution 8:[8]
If you want to keep animation and all constraints, you should find a negative constraint and make it positive before presenting alert controller.
// 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)
Solution 9:[9]
Here the function that I use to solve the issue. The issue appears because the constraint is being minus that I don't know why.
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)
}
Solution 10:[10]
Create view extension for getting all constraints
extension UIView {
func callRecursively(_ body: (_ subview: UIView) -> Void) {
body(self)
subviews.forEach { $0.callRecursively(body) }
}
}
Create UIAlertController extension to find all constraints with -16 constant and change it priority to 999
extension UIAlertController {
func fixConstraints() -> UIAlertController {
view.callRecursively { subview in
subview.constraints
.filter({ $0.constant == -16 })
.forEach({ $0.priority = UILayoutPriority(rawValue: 999)})
}
return self
}
}
Create your alert and call fixConstraints() while presenting:
let alert = UIAlertController(...
...
present(alert.fixConstraints(), animated: true, completion: nil)
Solution 11:[11]
everyone, I think I figured it out. The problem is that when the popoverPresentationController sourceView is assigned the self.view of the UIAlertController, a circular reference occurs, and the constraints break. sourceView should be assigned the view that invoked the popup, not the popup itself.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow

