In last week’s Swift Talk episode, Refactoring Imperative Layout Code, we refactored an flow layout algorithm into an algorithm with a . In this article, we’ll implement a justified version of this flow layout.

Thanks to the functional interface, we can plug it in by changing a single line of . With no implicit state, the interface makes it easy to swap out implementations, and also makes it possible to write unit tests without having to instantiate UIViews.

A justified flow layout aligns the left and right elements to the left and right margins and adjusts the spacing in between the elements accordingly. Here’s what our starting point looks like, with all lines aligned to the left:

- 2018 01 09 flowlayout b50cd1de - A Functional Interface for Imperative Code

We start by adding some handy extensions on Sequence and Array. We need a way to compute the sum of a sequence, the total width of an array of sizes (with some spacing in between), and the maximum height of an array of sizes:

extension Sequence where Element: Numeric {
    var sum: Element {
        return self.reduce(0 as Element, +)

extension Array where Element == CGSize {
    func width(spacing: CGFloat = 0) -> CGFloat {
        return { $0.width }.sum + spacing * CGFloat(count-1)

    var height: CGFloat {
        return { $0.height }.max() ?? 0

For the justified flow layout, the first step is to break our elements into lines. A line is an array of CGSizes, and we use a for-loop to iterate over the elements, adding each element if it still fits. If the line becomes too wide, we start a new line.

func justifiedFlowLayout(containerSize: CGSize, spacing: UIOffset, sizes: [CGSize]) -> [CGRect] {
    var lines: [[CGSize]] = [[]]
    for element in sizes {
        let lastline = lines.last!
        let projectedWidth = lastline.width(spacing: spacing.horizontal) + element.width + spacing.horizontal
        if projectedWidth > containerSize.width && !lastline.isEmpty {

    // ...

After having grouped the elements into lines, we can proceed to compute the frame for each element. We need to calculate the actual spacing between the elements in the line, so that a line fills the width of the container: we add up the elements’ widths in a line using line.width(spacing: 0), subtract that value from the container’s width and divide it by the number of spaces in the line. Since actualSpacing might not be an integer, we use .integral on all frames to convert any fractional numbers into integers, as setting a frame with fractional numbers can result in blurry rendering.

    // ...
    var result: [CGRect] = []
    var current: CGPoint = .zero
    for line in lines {
        let width = line.width(spacing: 0)
        let actualSpacing = (containerSize.width - width) / CGFloat(line.count - 1)
        for element in line {
            result.append(CGRect(origin: current, size: element).integral)
            current.x += element.width + actualSpacing

        current.y += line.height + spacing.vertical
        current.x = 0
    return result

That’s our justifiedFlowLayout function finished. In our ButtonsView, we can change the layout from a flow layout into a justified flow layout by changing a single function call:

final class ButtonsView: UIView {
    override func layoutSubviews() {

        let sizes = { $0.intrinsicContentSize }
        let spacing = UIOffset(horizontal: , vertical: )
        let frames = justifiedFlowLayout(containerSize: bounds.size, spacing: spacing, sizes: sizes)
        for (idx, frame) in frames.enumerated() {
            subviews[idx].frame = frame

Now our view adjusts the spacing in between elements to make sure the line is aligned to both the left and right margins:

- 2018 01 09 justifiedflowlayout 490f4402 - A Functional Interface for Imperative Code

For the implementation of both methods we wrote very procedural code, which seemed the natural way to write it. Swift allows us to write procedural code and have a functional interface, truly the best of both worlds.

Source link
Based Blockchain Network


Please enter your comment!
Please enter your name here