'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?

Output I get

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:

  1. Subclass your own Alert Controller from UIAlertController
  2. 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.