How FlowFrameLayout Automatically Wraps Views to the Next Line in FrameLayoutKit

FlowFrameLayout tracks remaining horizontal space in calculateSize(fitSize:) and instantiates new StackFrameLayout containers whenever the cumulative view widths exceed the available bounds, creating a dynamic, self-wrapping grid.

FlowFrameLayout is a dynamic layout container in the open-source FrameLayoutKit repository that arranges views in a flowing grid, automatically moving items to the next line when horizontal space runs out. Unlike static collection views, this Swift component performs wrapping calculations during the layout pass using intrinsic content sizes and configurable spacing parameters.

The Wrapping Algorithm in calculateSize(fitSize:)

The core wrapping logic lives in FlowFrameLayout.swift within the calculateSize(fitSize:) method. When the layout’s axis is set to horizontal, the algorithm iterates through the views array while maintaining a running tally of available horizontal space using a remainingSize variable.

Measuring and Consuming Space

For each view, the code calls sizeThatFits(_:) to determine the view’s intrinsic dimensions. It then subtracts the view’s width plus the interItemSpacing value from remainingSize.width. This consumption pattern continues sequentially until an overflow condition is detected.

Detecting Line Breaks (Lines 77–94)

The critical wrapping condition appears around lines 77–94 of FlowFrameLayout.swift. When remainingSize.width becomes negative and at least one view has already been placed in the current row (col > 1), the algorithm triggers a line break:

if remainingSize.width < 0, col > 1 {
    // Start a new row
    remainingSize.width = fitSize.width
    remainingSize.height -= result.height
    row += 1
    col = 1
}

This reset operation finalizes the current row’s dimensions and initializes a new entry in the sizeMap dictionary, which tracks how many items belong to each row using the format [row: numberOfItems].

Building Rows in layoutSubviews()

After calculating dimensions, layoutSubviews() constructs the actual interface using the row mapping generated during the size calculation phase.

Creating Row Containers

The method instantiates StackFrameLayout objects through a newStack() helper factory. Each StackFrameLayout represents a single row in the flow layout:

let map = contentSize.map
stackLayout.removeAll()
var index = 0
for i in 0..<map.keys.count {
    let stack = newStack()                 // ← a new row (StackFrameLayout)
    let numberOfItems = map[i+1] ?? 0
    for _ in 0..<numberOfItems {
        let view = views[index]
        stack + view                       // add view to the row
        index += 1
    }
    stackLayout + stack                    // add the row to the vertical scroll stack
    onNewStackBlock?(self, stack)         // callback for each new row
}

The Vertical Scroll Container

stackLayout is a ScrollStackView configured with a vertical axis. By stacking each StackFrameLayout vertically, the component creates the visual appearance of wrapped lines while maintaining a clean parent-child relationship in the view hierarchy.

Configuration and Callbacks

Handling Vertical Axis Layouts

When axis is set to vertical, the same logic applies but tracks remainingSize.height instead. In this orientation, "rows" become columns, and the layout wraps vertically rather than horizontally.

The onNewStackBlock Callback

The onNewStackBlock closure fires each time layoutSubviews() creates a new row, allowing developers to customize row appearance or execute side effects during the layout process.

Complete Implementation Example

The following Swift code demonstrates a horizontal flow layout that automatically wraps buttons to new lines based on screen width:

import FrameLayoutKit

// Create a flow layout that wraps horizontally
let flow = FlowFrameLayout()
flow.axis = .horizontal               // default – rows flow left-to-right
flow.interItemSpacing = 8
flow.lineSpacing = 12
flow.distribution = .left             // left-aligned rows

// Add views with intrinsic sizes
for i in 0..<20 {
    let btn = UIButton(type: .system)
    btn.setTitle("Item \(i)", for: .normal)
    btn.sizeToFit()                   // establish intrinsic size
    flow.add(btn)
}

// Implementation in a view controller
class DemoVC: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.addSubview(flow)
        flow.frame = view.bounds.insetBy(dx: 16, dy: 16)
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // Recalculates wrapping on rotation or resize
        flow.layoutIfNeeded()
    }
}

When the device rotates or the bounds change, calculateSize(fitSize:) recomputes the row mappings and layoutSubviews() rebuilds the hierarchy with the correct number of items per line.

Summary

  • FlowFrameLayout wraps views by monitoring remaining horizontal space during the calculateSize(fitSize:) pass
  • The overflow condition (remainingSize.width < 0 && col > 1) triggers creation of new rows around lines 77–94 in FlowFrameLayout.swift
  • A sizeMap dictionary records item counts per row for use during the actual layout phase
  • StackFrameLayout instances represent individual rows, stacked vertically inside a ScrollStackView
  • The layout automatically adapts to size changes by recalculating mappings whenever layoutSubviews() executes

Frequently Asked Questions

What triggers a line break in FlowFrameLayout?

A line break occurs when the cumulative width of views in the current row exceeds the available horizontal space. Specifically, when remainingSize.width becomes negative and at least one view is already placed (col > 1), the algorithm resets the remaining width counter and increments the row index.

How does FlowFrameLayout handle different screen sizes?

The layout recalculates row mappings whenever layoutSubviews() is called, such as during device rotation or container resizing. The calculateSize(fitSize:) method receives the new available bounds and redistributes views across rows accordingly, ensuring content reflows automatically without manual intervention.

What is the difference between interItemSpacing and lineSpacing?

interItemSpacing controls the horizontal gap between views within the same row, while lineSpacing determines the vertical distance between separate rows. These properties allow fine-grained control over both the inline distribution and the overall density of the grid layout.

Can FlowFrameLayout handle vertical wrapping?

Yes, when the axis property is set to vertical, the component tracks remainingSize.height instead of width and creates columns rather than rows. The wrapping logic remains identical, but views stack vertically and overflow to new columns when height constraints are exceeded.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →