Using Autolayout Visual Format with Swift?
Asked Answered
D

9

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

Dagenham answered 12/6, 2014 at 16:17 Comment(2)
Note that Swift dictionaries use [] and not {}Shoshonean
I don't get the same error you get if I copy & paste this into a playground...Hungarian
L
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!

Lied answered 12/6, 2014 at 16:27 Comment(4)
This does not compile for me (Xcode 6.0.1), probably because the 1st and 4th parameters do not accept nil.Doerrer
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 youTameratamerlane
Xcode 7 tells me that options is now: NSLayoutFormatOptions(rawValue: 0)Contemn
As juanjo mentioned, there is no need to do NSLayoutFormatOptions(rawValue: 0). Rather just type in []Miamiami
D
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))
Diley answered 17/6, 2014 at 19:25 Comment(0)
A
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)
Aggappe answered 12/6, 2014 at 16:23 Comment(0)
I
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)
    }
Irritation answered 5/9, 2014 at 11:43 Comment(3)
BOOM, where allZeros come from and why does it work. Focking swift.Rockefeller
:-D ... didn't try it with the current version ... 5.Sep it worked ;-)Irritation
allZeros is not defined with XCode 7.3. Use NSLayoutFormatOptions(rawValue: 0) as indicated above.Dried
I
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
Ipomoea answered 23/1, 2015 at 6:53 Comment(1)
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.Ipomoea
C
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: [:])
Cohby answered 19/11, 2015 at 18:42 Comment(0)
A
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]-|")
Alumnus answered 20/5, 2015 at 14:32 Comment(2)
@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...Alumnus
I appreciate that you have checked the code. I had forgotten my situation, But, I guess you are right. :)Northern
R
0

You have to access to the struct NSLayoutFormatOptions.

Following works for me.

self.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("",
options:NSLayoutFormatOptions.AlignAllBaseline,
metrics: nil, views: nil))
Rationale answered 2/7, 2014 at 18:11 Comment(0)
U
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)
Unexceptionable answered 12/7, 2014 at 20:50 Comment(1)
Using normal Swift dictionaries such as let views: [NSObject : AnyObject] = ["icon" : iconImageView] apparently works fine in Xcode 6.1.1.Petrarch

© 2022 - 2024 — McMap. All rights reserved.