Swift default AlertViewController breaking constraints
IosSwiftIphoneXcodeIos AutolayoutIos Problem Overview
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.
Ios Solutions
Solution 1 - Ios
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 - Ios
This error is not critical, seems to be unfixed bug form Apple. This constraint appears in animation style just after presenting. I tried to catch and change it (change values, relations, priority) before presenting – no success because of this dynamically added constraints.
When you turn off animation in self.present(alert, animated: false)
and using alert.view.addSubview(UIView())
– the error disappears. I can't explain it, but it works!
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)
Solution 3 - Ios
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 4 - Ios
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 5 - Ios
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 6 - Ios
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 7 - Ios
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 8 - Ios
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 9 - Ios
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 10 - Ios
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 11 - Ios
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 12 - Ios
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.