acb's technical journal

Swift snippets: flatMap and layout constraints

Since version 6, iOS has had a constraint-based layout engine for laying out views The constraint-based layout engine in iOS is considerably more powerful than the old system of auto-layout hints, allowing reasonably sophisticated flexible layouts to be specified purely as systems of constraints. However, it can be cumbersome to set up if creating all constraints individually; any reasonably complicated layout will soon have dozens of NSLayoutConstraint(item:attribute:relatedBy:toItem:attribute:multiplier:constant:) lines padding out its construction code, making one wonder why not just cut the Gordian knot and write code to manually lay out the view, as in the old days?

Fortunately, Apple provide a shortcut: the NSLayoutConstraint.withVisualFormat method; which takes a string describing the relationships between elements in an ASCII-art-style syntax known as the Visual Format Language, a few options and a dictionary of elements and emits a list of NSLayoutConstraints, as so:

let viewsDict:[NSObject:AnyObject] = [
    "lBtn" : leftButton,
    "rBtn" : rightBtn,
    "title" : titleLabel
]
let constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-12-[lBtn(30)]-6-[title]-6-[rBtn(30)]-12-|", 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict)
myView.addConstraints(constraints)

As implied, each constraintsWithVisualFormat call takes a line of visual format language and produces an array of zero or more constraints; the line above, for example, would produce six; four distance constraints and two size constraints. However, one such line is rarely enough to unambiguously specify the constraints for a view and its contents; most layouts would require more than one line of visual format language to specify their constraints. The above, for example, covers only horizontal constraints; adding vertical ones to the mix would involve a second line, like so:

let constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-12-[lBtn(30)]-6-[title]-6-[rBtn(30)]-|", 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict) + 
    NSLayoutConstraint.constraintsWithVisualFormat("V:|-10-[title(20)]", 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict)
myView.addConstraints(constraints)
Which gives two more constraints on the title label view: a distance from the top edge of the superview and a height. But what about the buttons?
let constraints = NSLayoutConstraint.constraintsWithVisualFormat("H:|-12-[lBtn(30)]-6-[title]-6-[rBtn(30)]-|", 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict) + 
    NSLayoutConstraint.constraintsWithVisualFormat("V:|-10-[title(20)]", 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict) +
    NSLayoutConstraint.constraintsWithVisualFormat("V:|-5-[lBtn(30)]", 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict) +
    NSLayoutConstraint.constraintsWithVisualFormat("V:|-5-[rBtn(30)]", 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict)
myView.addConstraints(constraints)
All of which soon starts to get somewhat unwieldy; there's a lot of repeated boilerplate there, which suggests that one could refactor it.

We could minimise the boilerplate by putting the visual format strings in an array of strings and mapping over them with constraintsWithVisualFormat. That would give us an array of arrays of constraints, which we could then flatten using reduce, like so:

let visualConstraints = [
  "H:|-12-[lBtn(30)]-6-[title]-6-[rBtn(30)]-|",
  "V:|-10-[title(20)]", ...
]
let constraints = visualConstraints.map { 
  NSLayoutConstraint.constraintsWithVisualFormat($0, 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict)
}.reduce([], { $0 + $1 })
myView.addConstraints(constraints)
Which is somewhat better, though it can be improved. As of Xcode 6.3, arrays (and optionals) in Swift will have a new operation named flatMap, which combines the map and flatten steps. In short, where a map takes a container of some values of type A and a function that converts an A to a B and returns a container of the same number of values of type B, flatMap takes the same array and a function that converts an A to a container of zero or more Bs, and returns a container of some number of Bs. In any case, with flatMap, the above code reduces to:
let visualConstraints = [
  "H:|-12-[lBtn(30)]-6-[title]-6-[rBtn(30)]-|",
  "V:|-10-[title(20)]", ...
]
let constraints = visualConstraints.flatMap { 
  NSLayoutConstraint.constraintsWithVisualFormat($0, 
        options:NSLayoutFormatOptions(0), metrics:nil, views:viewsDict)
}
myView.addConstraints(constraints)
flatMap is a useful operation, and one whose uses one can see in many places. In general, whenever a process produces zero or more items of output for each input, one wants to use a flatMap to handle them. Swift is also introducing flatMap on Optionals (which, of course, may be seen as a container holding zero or one items of a type), where it can be used for chaining a number of functions which may or may not yield a value:
func getCurrentUserIcon(session: Session?) -> UIImage? {
  return getUser(session).flatMap { getIconForUser($0) }
}
This is a little like Swift's optional chaining, with the key difference that, while optional chaining is limited to calling the underlying objects' methods, flatMap can apply any expression yielding an Optional; a move away from the object-oriented paradigm of objects and methods towards more functional techniques.

Unsurprisingly, flatMap is much more common in functional programming. The presence of a flatMap operation is one of the defining criteria of a pattern known as the monad, which, in a functional paradigm, can be used to define everything from container types to asynchronous operations to ways of compartmentalising state in functions without side-effects, in a way that follows consistent laws. Languages like Haskell and Scala use monads extensively, defining the relevant types in a consistently monadic fashion. And while Swift is not a functional language per se, it has been speculated for a while that it may be moving in an increasingly functional direction (albeit perhaps sufficiently gradually as not to alienate old Objective C hands); the arrival of flatMap could be more grist to this mill.

There are no comments yet on "Swift snippets: flatMap and layout constraints"

Want to say something? Do so here.

Post pseudonymously

Display name:
URL:(optional)
To prove that you are not a bot,
please enter the text in the image on the right
in the field below it.

Your Comment:

Please keep comments on topic and to the point. Inappropriate comments may be deleted.

Note that markup is stripped from comments; URLs will be automatically converted into links.