89

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.

linus_hologram
  • 1,595
  • 13
  • 38
  • do you construct any other constraints in code ? – Shehata Gamal Apr 12 '19 at 13:54
  • @Sh_Khan no I don't. I mean, obviously I have other views underneath the Alert View but they work fine and do not produce any constraint errors. But I did not change anything at the AlertView constraints – linus_hologram Apr 12 '19 at 13:58
  • 1
    @linus_hologram, does thing causes any visual glitch on the screen? if __yes__ then what? if __no__, don't waste your time on trying to fix something which is not even broken. – holex Apr 12 '19 at 14:00
  • @holex no it does not cause any glitches. It works fine - that's why I am so confused about it. I only thought that an app must not have any bugs or error logs when submitting it, that's why I thought I should fix it. – linus_hologram Apr 12 '19 at 14:01
  • 1
    Constraints log isn't always true , sometimes it produces misleading things – Shehata Gamal Apr 12 '19 at 14:05
  • That's a mass with your constraints, not the alert view, most probably. If you deactivate , the error should gone, but may disrupt something else. – Vanya Apr 12 '19 at 14:25
  • @Vanya How can I deactivate that specific constraint? According to the ViewHierarchy debugger, this constraint belongs to the actual UIAlertViewController and not to any of my views. To be more specific, it belongs to the title of the AlertViewController. – linus_hologram Apr 12 '19 at 14:29
  • @linus_hologram that was just my guess, I cannot see a problem with your code and nobody else, so the problematic part is more likely missing. – Vanya Apr 12 '19 at 15:31
  • What's with the image of the green mouse? Is that part of your action sheet? Is the error related to that view? – rmaddy Apr 12 '19 at 15:50
  • No, that should just show that there will be a button underneath in the future.... and was not intended to be a mouse xD – linus_hologram Apr 12 '19 at 15:54
  • 1
    I have examined the issue with debugger, it is dynamically added constraint... I can't find this constraint before presenting an UIAlertController... See more in my answer below – Agisight Apr 12 '19 at 16:18
  • My app just got rejected for a crash I traced back to this: it appears that the warning in the simulator translates to actual crashes on iPads. I'm looking more into this. – cdf1982 Jun 18 '19 at 15:40
  • Thanks! Please let me know what you find out. @cdf1982 – linus_hologram Jun 18 '19 at 16:16
  • I took the easiest way out, since this bug seems quite frequent and not fixed yet: I replaced UIAlertViewControllers actionSheets with [DYAlertController](https://github.com/DominikButz/DYAlertController). I don’t love replacing a native controller with a library, but it worked just fine. – cdf1982 Jun 19 '19 at 12:44

12 Answers12

53

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()
Rodrigo Fava
  • 318
  • 3
  • 12
jims1103
  • 733
  • 5
  • 8
  • 1
    Works great, no more log pollution! – Klaas Nov 20 '19 at 11:40
  • This is actually causing more problems now (iOS 13.2.2), not sure how it worked on earlier versions. Causing UIAlertController to show pinned to the top of the screen. – jovanjovanovic Nov 28 '19 at 08:04
  • @jovanjovanovic sorry to hear that you're experiencing that, but I can't reproduce what you're describing. This solution still stands as the one that removes the warnings without adverse impact. If you have a code sample that can reproduce what you're describing, please share. – jims1103 Dec 02 '19 at 20:31
  • I use this cose just after creating the Alert, but seems not working! Where is the best place to put the code? – Lucas Tegliabue Dec 19 '19 at 10:59
  • @LucasTegliabue use it before presenting alertcontroller like let alertController = UIAlertController() alertController.pruneNegativeWidthConstraints() self.present(alertController, animated: true, completion: nil) – Jack Dec 20 '19 at 21:14
  • Takes for this tip. Just used it on Xcode 11.4 and iOS13.4 Works perfectly to get rid of the Xcode bug log warning – mac_eric Mar 28 '20 at 07:56
  • The problem seems to be fixed in iOS 14.5 (Swift 5/Xcode 12.5/iOS 12.3 project), with the latest iPhone SE2 simulator there's no error, even without calling `pruneNegativeWidthConstraints`. Unfortunately this extension doesn't work with an iPhone SE (iOS 12.4) simulator: I'm calling the function after `alert.addAction` & `alert.view.tintColor = ...` and before `self.present`but the complaint is still spammed in console every time I open the action sheet. – Neph May 05 '21 at 11:40
43

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)
Agisight
  • 1,778
  • 1
  • 14
  • 15
  • 1
    how is this answer related to the constraint-issue the OP has? – holex Apr 12 '19 at 13:57
  • Thanks for your answer, unfortunately this does not fix the problem. I still get exactly the same constraint error. – linus_hologram Apr 12 '19 at 13:57
  • 4
    turn off the animation + `alert.view.addSubview(UIView())` worked for me. But without the animation, feels so weird... Thanks for this solution anyway. – Tieda Wei Apr 15 '19 at 20:53
  • This doesn't really solve the problem unfortunately. Just provides a bad user experience. – Edward May 03 '19 at 19:52
  • 1
    Yes, I explain it as bug from Apple. I explained it. But if you can to solve in, you can try to get needed constraint and change it. I think that is not a good idea, cuz it is a bug. – Agisight May 03 '19 at 21:32
  • FYI a radar has been filed here : http://openradar.appspot.com/49289931 – Bioche May 08 '19 at 11:17
  • in my case, turning off animation resulted in the layout constraint messages showing first time alert was presented and then no messages all subsequent times. When I also added the dummy UIView() to the alert, the message did not happen even the first time. – user2132980 May 26 '19 at 13:39
  • That's right! Sometimes it works without error if we use custom UIView. – Agisight May 26 '19 at 14:15
  • 1
    I think that adding a subview forces the autolayout constraints being re-calculated, silencing the warning. – FormigaNinja Jul 09 '19 at 06:57
  • 3
    This issue occurs when `animation` is set to true. I think it is an internal bug and it can be ignored since the actoinSheet shows properly. I am glad I could narrow it down to the root cause, at first I taught it was my code. – Jerry Okafor Jul 19 '19 at 21:06
  • animation false doesnt help at all. I still get the same error in ios 13.2 – Nic Wanavit Nov 23 '19 at 16:34
20

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.

Andrei Konstantinov
  • 6,971
  • 4
  • 41
  • 57
  • 1
    Looks like they fixed it in iOS 14.5 - tested in a Swift 5/Xcode 12.5/iOS 12.3 project with the iPhone SE2 simulator. In the same project the iPhone SE 12.4 simulator logs the message in console. – Neph May 05 '21 at 12:03
8

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)
            }
        }
    }
}
Joe Schofield
  • 124
  • 1
  • 4
2

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)
Josh Bernfeld
  • 4,246
  • 2
  • 32
  • 35
  • I tried all proposed solutions applicable in Swift without success. I am using XCode 11.5, compiling for ios 13.0, running in iPhone Xs Max. Still getting `""` . I can't help much if not by telling you the issue is still there. As somebody suggested, I will just ignore the error, but i don't like it. – Nicola Mingotti Jun 01 '20 at 22:17
  • it doesn't work at all. The solution from RichW works at least on iOS 13 – Vyachaslav Gerchicov Aug 17 '20 at 11:26
  • @VyachaslavGerchicov Be careful though, I believe that older iOS versions would crash when changing a required constraint to nonrequired at runtime. https://stackoverflow.com/questions/31186187/how-can-i-change-constraints-priority-in-run-time – Josh Bernfeld Aug 17 '20 at 18:21
1

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.

RichW
  • 127
  • 1
  • 4
  • Works in iOS 13 but doesn't in iOS 12. Tried on iPhone X simulator – Vyachaslav Gerchicov Aug 17 '20 at 11:24
  • @RichW this is a good approach. Be careful though, I believe that older iOS versions would crash when changing a required constraint to nonrequired at runtime. https://stackoverflow.com/questions/31186187/how-can-i-change-constraints-priority-in-run-time – Josh Bernfeld Aug 17 '20 at 18:19
1

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.

user2430797
  • 320
  • 5
  • 18
0

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
    
0

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)
Denis Bystruev
  • 326
  • 4
  • 11
0

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

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)
Jeff Bootsholz
  • 2,971
  • 15
  • 70
  • 141
gaRik
  • 607
  • 6
  • 10
0

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.

kode54
  • 61
  • 2