Swift default AlertViewController breaking constraints

IosSwiftIphoneXcodeIos Autolayout

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

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.

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. enter image description here 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:

  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 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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionlinus_hologramView Question on Stackoverflow
Solution 1 - Iosjims1103View Answer on Stackoverflow
Solution 2 - IosAgisightView Answer on Stackoverflow
Solution 3 - IosAndrei KonstantinovView Answer on Stackoverflow
Solution 4 - IosJoe SchofieldView Answer on Stackoverflow
Solution 5 - IosJosh BernfeldView Answer on Stackoverflow
Solution 6 - IosRichWView Answer on Stackoverflow
Solution 7 - Iosuser2430797View Answer on Stackoverflow
Solution 8 - IosAlexandr KirilovView Answer on Stackoverflow
Solution 9 - IosDenis BystruevView Answer on Stackoverflow
Solution 10 - IosArif LuthfiansyahView Answer on Stackoverflow
Solution 11 - IosgaRikView Answer on Stackoverflow
Solution 12 - Ioskode54View Answer on Stackoverflow