Search code examples
macoscocoaswiftautolayoutvisual-format-language

Aligning NSView inside NSView using VFL


I am having a Subview(say MainView) of NSView, with NSTextField inside it.

Note: This code is based on this github.repo

MainView has designated initializer as follows

init(title : String, layout :NSLayoutAttribute) {
    innerView = self.createTextField(title)//Text field with stringValue = title
    mainView.addSubview(innerView)
    createViewConstrants(innerView, layout: layout)
}

layout can be Left, Right or CenterX.
(Inner TextView will be vertically centered always, only Horizontal alignment can vary)


Following function makes constraint for the purpose

func createViewConstrants(view : NSView, layout :NSLayoutAttribute) {

    let viewMap = ["innerView" : view, "superview" : mainView]
    var constraints :[AnyObject]!

    switch layout {
    case .Left:
        println("Left")
        constraints = NSLayoutConstraint.constraintsWithVisualFormat(
            "H:[superview]-(<=1)-[innerView]",
            options: NSLayoutFormatOptions.AlignAllCenterY,
            metrics: nil,
            views: viewMap)
        mainView.addConstraints(constraints)

    case .Right:
        println("Right")
        constraints = NSLayoutConstraint.constraintsWithVisualFormat(
            "H:[superview]-(<=1)-[innerView]-|",
            options: NSLayoutFormatOptions.AlignAllCenterY,
            metrics: nil,
            views: viewMap)
        mainView.addConstraints(constraints)

    case .CenterX:
        println("Center")

        constraints = NSLayoutConstraint.constraintsWithVisualFormat(
            "V:[superview]-(<=1)-[innerView]",
            options: NSLayoutFormatOptions.AlignAllCenterX,
            metrics: nil,
            views: viewMap)
        mainView.addConstraints(constraints)

        constraints = NSLayoutConstraint.constraintsWithVisualFormat(
            "H:[superview]-(<=1)-[innerView]",
            options: NSLayoutFormatOptions.AlignAllCenterY,
            metrics: nil,
            views: viewMap)
        mainView.addConstraints(constraints)

    default:
        fatalError("Invalid layout")

    }
}

Align Right and Center are working fine, but Align left is not working.

Also since the vertically centering is common to all the cases can it be taken out of the switch case ?


Solution

  • Several issues:

    1. You don't normally list the superview explicitly in VFL. Rather, you use the vertical line (|) character. This:

      H:[superview]-(<=1)-[innerView]
      

      doesn't mean that innerView's leading edge is less than 1 point "after" the superview's leading edge. It means that innerView's leading edge is less than 1 point after the superview's trailing edge, which could put innerView outside of its superview's bounds, which would cause it to be clipped.

    2. If these are the only constraints you're adding that affect innerView, then your layout is ambiguous. An inequality allows any of the possible values and the system has no reliable way of picking one of those values over any other.

    3. You can't really express centering in the VFL. You can pass along an alignment flag to constraintsWithVisualFormat(_:options:metrics:views:) (as you're aware), but a) that's not really part of the visual format language as such and b) you can't do that without also creating some bogus, unwanted constraints in the other orientation. That is, you can't use the alignment flag to center things vertically unless you also create some bogus horizontal constraints. (And vice versa.) That's because the alignment flags only apply to the views listed in the VFL string and the VFL string always implies constraints.

    4. Why do you insist on using VFL? If you use init(item:attribute:relatedBy:toItem:attribute:multiplier:constant:), you can simply pass along the layout parameter that your method receives. There would be no need for a switch with multiple cases. You would just do:

      var constraint = NSLayoutConstraint(item: innerView,
                                     attribute: layout,
                                     relatedBy: .Equal,
                                        toItem: mainView,
                                     attribute: layout
                                    multiplier: 1,
                                      constant: 0)
      mainView.addConstraint(constraint)
      constraint = NSLayoutConstraint(item: innerView,
                                 attribute: .CenterY,
                                 relatedBy: .Equal,
                                    toItem: mainView,
                                 attribute: .CenterY
                                multiplier: 1,
                                  constant: 0)
      mainView.addConstraint(constraint)
      

      That would handle all three cases.