33

I've been trying to use the Autolayout Visual Format Language in Swift, using NSLayoutConstraint.constraintsWithVisualFormat. Here's an example of some code that does nothing useful, but as far as I can tell should make the type checker happy:

let foo:[AnyObject]! = NSLayoutConstraint.constraintsWithVisualFormat(
  format: "", options: 0, metrics: {}, views: {})

However, this triggers the compiler error:

"Cannot convert the expression's type '[AnyObject]!' to type 'String!'".

Before I assume this is a Radar-worthy bug, is there anything obvious I'm missing here? This happens even without the explicit casting of the variable name, or with other gratuitous downcasting using as. I can't see any reason why the compiler would be expecting any part of this to resolve to a String!.

sudo make install
  • 5,629
  • 3
  • 36
  • 48
Mike Walker
  • 378
  • 1
  • 3
  • 8

9 Answers9

67

this works for me with no error:

let bar:[AnyObject]! = NSLayoutConstraint.constraintsWithVisualFormat(
  nil, options: NSLayoutFormatOptions(0), metrics: nil, views: nil)

update

the line above may not be compiled since the 1st and 4th parameters cannot be optionals anymore.

syntactically those have to be set, like e.g. this:

let bar:[AnyObject] = NSLayoutConstraint.constraintsWithVisualFormat("", options: NSLayoutFormatOptions(0), metrics: nil, views: ["": self.view])

update

(for Xcode 7, Swift 2.0)

the valid syntax now requests the parameters's name as well, like:

NSLayoutFormatOptions(rawValue: 0)

NOTE: this line of code shows the correct syntax only, the parameters itself won't guarantee the constraint will be correct or even valid!

holex
  • 23,961
  • 7
  • 62
  • 76
  • This does not compile for me (Xcode 6.0.1), probably because the 1st and 4th parameters do not accept `nil`. – Pang Oct 14 '14 at 08:26
  • `NSLayoutFormatOptions(0)` saved me! Just using `0` for the options as you would in obj-c doesn't work and doesn't tell you why in the error...Thank you – Sakiboy Jul 06 '15 at 20:59
  • 3
    Xcode 7 tells me that options is now: `NSLayoutFormatOptions(rawValue: 0)` – blwinters Sep 26 '15 at 23:23
  • 3
    As juanjo mentioned, there is no need to do `NSLayoutFormatOptions(rawValue: 0)`. Rather just type in `[]` – funct7 Apr 28 '16 at 00:49
13

The first gotcha here is that Swift Dictionary is not yet bridged with NSDictionary. To get this to work, you'll want to explicitly create a NSDictionary for each NSDictionary-typed parameters.

Also, as Spencer Hall points out, {} isn't a dictionary literal in Swift. The empty dictionary is written:

[:]

As of XCode 6 Beta 2, this solution allows you to create constraints with the visual format:

var viewBindingsDict: NSMutableDictionary = NSMutableDictionary()
viewBindingsDict.setValue(fooView, forKey: "fooView")
viewBindingsDict.setValue(barView, forKey: "barView")
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-[fooView]-[barView]-|", options: nil, metrics: nil, views: viewBindingsDict))
Community
  • 1
  • 1
John M. P. Knox
  • 713
  • 4
  • 11
5

Try this - remove the initial variable name (format:), use NSLayoutFormatOptions(0), and just pass nil for those optional NSDictionaries:

let foo:[AnyObject]! = NSLayoutConstraint.constraintsWithVisualFormat("", options: NSLayoutFormatOptions(0), metrics: nil, views: nil)
Nate Cook
  • 92,417
  • 32
  • 217
  • 178
5

FYI: if you use views with constraintWithVisualFormat - instead of wrapping with NSMutableDict

["myLabel": self.myLabel!]

and to be more specific

var constraints = [NSLayoutConstraint]()
NSLayoutConstraint.constraintsWithVisualFormat("H:|-15-[myLabel]-15-|", 
    options:NSLayoutFormatOptions.allZeros, 
    metrics: nil, 
    views: ["myLabel": self.myLabel!]).map {
        constraints.append($0 as NSLayoutConstraint)
    }
StrangeDays
  • 352
  • 5
  • 4
4

This works with Xcode 6.1.1:

extension NSView {

    func addConstraints(constraintsVFL: String, views: [String: NSView], metrics: [NSObject: AnyObject]? = nil, options: NSLayoutFormatOptions = NSLayoutFormatOptions.allZeros) {
        let mutableDict = (views as NSDictionary).mutableCopy() as NSMutableDictionary
        let constraints = NSLayoutConstraint.constraintsWithVisualFormat(constraintsVFL, options: options, metrics: metrics, views: mutableDict)
        self.addConstraints(constraints)
    }

}

Then you can use calls like:

var views : [String: NSView] = ["box": self.box]
self.view.addConstraints("V:[box(100)]", views: views)

This works to add constraints. If you are using iOS, substitute UIView for NSView


You should probably check out

  • Cartography, which is a new approach, but quite awesome. It uses Autolayout under the hood.
  • SnapKit, which I haven't tried but is also a DSL autolayout framework
Dan Rosenstark
  • 68,471
  • 58
  • 283
  • 421
  • Even if you use Cartography or Snapkit, you need to read the docs on Autolayout to understand why certain things work the way they do. Also, and this is advice that's useful at any time, have a separate project in which you are constantly trying new things out in a clean environment. Mine is call "PlayingWithUI" and it uses a Git branch for every new concept I want to know about, from Carthage integration to Autolayout to working with certain control types. – Dan Rosenstark Jun 06 '16 at 13:17
3

NSLayoutFormatOptions implements the OptionSetType protocol, which inherits from SetAlgebraType which inherits from ArrayLiteralConvertible, so you can initialise NSLayoutFormatOptions like this: [] or this: [.DirectionLeftToRight, .AlignAllTop]

So, you can create the layout constraints like this:

NSLayoutConstraint.constraintsWithVisualFormat("", options: [], metrics: nil, views: [:])
juanjo
  • 3,737
  • 3
  • 39
  • 44
1

It slightly annoys me that I'm calling NSLayoutConstraint (singular) to generate constraintsWithVisualFormat... (plural), though I'm sure that's just me. In any case, I have these two top level functions:

snippet 1 (Swift 1.2)

#if os(iOS)
    public typealias View = UIView
#elseif os(OSX)
    public typealias View = NSView
#endif

public func NSLayoutConstraints(visualFormat: String, options: NSLayoutFormatOptions = .allZeros, views: View...) -> [NSLayoutConstraint] {
    return NSLayoutConstraints(visualFormat, options: options, views: views)
}

public func NSLayoutConstraints(visualFormat: String, options: NSLayoutFormatOptions = .allZeros, views: [View] = []) -> [NSLayoutConstraint] {
    if visualFormat.hasPrefix("B:") {
        let h = NSLayoutConstraints("H\(dropFirst(visualFormat))", options: options, views: views)
        let v = NSLayoutConstraints("V\(dropFirst(visualFormat))", options: options, views: views)
        return h + v
    }
    var dict: [String:View] = [:]
    for (i, v) in enumerate(views) {
        dict["v\(i + 1)"] = v
    }
    let format = visualFormat.stringByReplacingOccurrencesOfString("[v]", withString: "[v1]")
    return NSLayoutConstraint.constraintsWithVisualFormat(format, options: options, metrics: nil, views: dict) as! [NSLayoutConstraint]
}

Which can be used like so:

superView.addConstraints(NSLayoutConstraints("B:|[v]|", view))

In other words, views are auto-named "v1" to "v\(views.count)" (except the first view which can be also referred to as "v"). In addition, prefixing the format with "B:" will generate both the "H:" and "V:" constraints. The example line of code above therefore means, "make sure the view always fits the superView".

And with the following extensions:

snippet 2

public extension View {

    // useMask of nil will not affect the views' translatesAutoresizingMaskIntoConstraints
    public func addConstraints(visualFormat: String, options: NSLayoutFormatOptions = .allZeros, useMask: Bool? = false, views: View...) {
        if let useMask = useMask {
            for view in views {
                #if os(iOS)
                    view.setTranslatesAutoresizingMaskIntoConstraints(useMask)
                #elseif os(OSX)
                    view.translatesAutoresizingMaskIntoConstraints = useMask
                #endif
            }
        }
        addConstraints(NSLayoutConstraints(visualFormat, options: options, views: views))
    }

    public func addSubview(view: View, constraints: String, options: NSLayoutFormatOptions = .allZeros, useMask: Bool? = false) {
        addSubview(view)
        addConstraints(constraints, options: options, useMask: useMask, views: view)
    }
}

We can do some common tasks much more elegantly, like adding a button at a standard offset from the bottom right corner:

superView.addSubview(button, constraints: "B:[v]-|")

For example, in an iOS playground:

import UIKit
import XCPlayground

// paste here `snippet 1` and `snippet 2`

let view = UIView(frame: CGRect(x: 0, y: 0, width: 500, height: 500))
XCPShowView("view", view)
view.backgroundColor = .orangeColor()
XCPShowView("view", view)
let button = UIButton(frame: CGRect(x: 0, y: 0, width: 50, height: 50))
button.setTitle("bottom right", forState: .Normal)

view.addSubview(button, constraints: "B:[v]-|")
Milos
  • 2,728
  • 22
  • 24
  • @WanbokChoi, I just checked the code on iOS and it works (at least in Swift 1.2). Have you forgotten to change `NSView` to `UIView` and the `translatesAutoresizingMaskIntoConstraints` to its `UIView` equivalent? In any case, I have updated the answer to do this automatically... – Milos Jul 30 '15 at 13:07
  • I appreciate that you have checked the code. I had forgotten my situation, But, I guess you are right. :) – Wanbok Choi Jul 31 '15 at 07:26
0

You have to access to the struct NSLayoutFormatOptions.

Following works for me.

self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("",
options:NSLayoutFormatOptions.AlignAllBaseline,
metrics: nil, views: nil))
user353gre3
  • 2,747
  • 4
  • 24
  • 27
0
// topLayoutGuide constraint
    var views: NSMutableDictionary = NSMutableDictionary()
    views.setValue(taskNameField, forKey: "taskNameField")
    views.setValue(self.topLayoutGuide, forKey: "topLayoutGuide")
    let verticalConstraint = "V:[topLayoutGuide]-20-[taskNameField]"
    let constraints:[AnyObject]! = NSLayoutConstraint.constraintsWithVisualFormat(verticalConstraint, options: NSLayoutFormatOptions(0), metrics: nil, views: views)
    self.view.addConstraints(constraints)

// bottomLayoutGuide constraint

    var views: NSMutableDictionary = NSMutableDictionary()
    views.setValue(logoutButton, forKey: "logoutButton")
    views.setValue(self.bottomLayoutGuide, forKey: "bottomLayoutGuide")
    let verticalConstraint = "V:[logoutButton]-20-[bottomLayoutGuide]"
    let constraints:[AnyObject]! = NSLayoutConstraint.constraintsWithVisualFormat(verticalConstraint, options: NSLayoutFormatOptions(0), metrics: nil, views: views)
    self.view.addConstraints(constraints)
Michael Peterson
  • 10,383
  • 3
  • 54
  • 51
  • Using normal Swift dictionaries such as `let views: [NSObject : AnyObject] = ["icon" : iconImageView]` apparently works fine in Xcode 6.1.1. – Drux Jan 25 '15 at 06:13