Search code examples
swiftmacosswift3autolayoutnslayoutconstraint

Swift addConstraint leads to EXC_BAD_INSTRUCTION


I've been playing around with NSTouchBar in a macOS application and came up with the following code:

func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
    switch identifier {
    case NSTouchBarItemIdentifier.testItemIdentifier:
        let viewObject = NSView()

        for i in 1...10 {
            let imageWidth: CGFloat = 20;
            let imageHeight: CGFloat = 20;
            let imagePositionTop: CGFloat = 0
            let imagePositionLeft: CGFloat = imageWidth * (CGFloat(i) - 1)

            let imageObject = NSImage(named: "test_image")!
            imageObject.size = NSSize(width: 20, height: 20)
            let imageView = NSImageView(image: imageObject)
            let constraintObject1 = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.top, relatedBy: NSLayoutRelation.equal, toItem: viewObject, attribute: NSLayoutAttribute.top, multiplier: 1, constant: imagePositionTop)
            let constraintObject2 = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.left, relatedBy: NSLayoutRelation.equal, toItem: viewObject, attribute: NSLayoutAttribute.left, multiplier: 1, constant: imagePositionLeft)
            let constraintObject3 = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: imageView, attribute: NSLayoutAttribute.width, multiplier: 0, constant: imageWidth)
            let constraintObject4 = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: imageView, attribute: NSLayoutAttribute.height, multiplier: 0, constant: imageHeight)

            imageView.addConstraint(constraintObject1)
            imageView.addConstraint(constraintObject2)
            imageView.addConstraint(constraintObject3)
            imageView.addConstraint(constraintObject4)

            viewObject.addSubview(imageView)
        }

        let customViewItem = NSCustomTouchBarItem(identifier: identifier)
        customViewItem.view = viewObject
        return customViewItem
    default:
        return nil
    }
}

When running the application, I get an EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0). The cause of this seems to be the calls to addConstraint. I've never created constraints manually before, and am relatively new to the swift language.

Any help is greatly appreciated.


Solution

  • @toddg is correct: you must add imageView as a subview of viewObject before you can activate a constraint between the two views. Two views must be in the same view hierarchy if you want to activate a constraint involving both of them.

    But you have other problems.

    A constraint between two views A and B must be added to a common ancestor of A and B (which may be A or B itself). In your case, since imageView is a subview of viewObject, that means that any constraint between imageView and viewObject must be added to viewObject (or a superview of viewObject). You cannot add the constraint to imageView. You can fix this by setting the isActive property of the constraint to true instead of using addConstraint. When you set the isActive property to true, the constraint will add itself to the appropriate view.

    Also, you're creating a constraint between imageView.width and imageView.width, which doesn't make sense. If you want to constrain imageView.width to a constant, the other item of the constraint should be nil with an attribute of .notAnAttribute.

    Also, you're not setting translatesAutoresizingMaskIntoConstraints to false, which you need to do if you want to use constraints to set the size and position of the imageView.

    Anyway, you're using a bunch of old API that makes things harder than necessary. You can make your code correct, and much shorter and simpler, by using an NSStackView. NSStackView was added in macOS 10.9, and NSTouchBar was added in macOS 10.12.2, so there's no reason not to use a stack view for this.

    func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {
        switch identifier {
        case NSTouchBarItemIdentifier.testItemIdentifier:
            let stack = NSStackView()
            stack.translatesAutoresizingMaskIntoConstraints = false
            stack.spacing = 0
            stack.distribution = .fillEqually
    
            let length = CGFloat(20)
    
            for _ in 1...10 {
                let imageObject = NSImage(named: "test_image")!
                imageObject.size = NSSize(width: length, height: length)
                let imageView = NSImageView(image: imageObject)
                imageView.translatesAutoresizingMaskIntoConstraints = false
                stack.addArrangedSubview(imageView)
            }
    
            let customViewItem = NSCustomTouchBarItem(identifier: identifier)
            customViewItem.view = stack
            return customViewItem
        default:
            return nil
        }
    }
    

    Result:

    enter image description here