Contents

tevelee/swiftui-flow

Introduces HFlow and VFlow similar to HStack and VStack.

Features

  • 📐 Wrapping layout — items flow onto new lines (or columns) when they run out of space.
  • ↔️ Two axesHFlow wraps rows, VFlow wraps columns.
  • 🎯 Per-axis alignment — align items within a line and lines within the layout.
  • ↕️ Independent spacing — set item spacing and line spacing separately.
  • ⚖️ Even distribution — balance items across lines with the Knuth–Plass algorithm.
  • ↔️ Justified lines — stretch lines to fill the available space.
  • 🪗 Flexibility model — let items grow, stay rigid, or claim a whole line.
  • 📏 Line limits — cap a flow to N lines with an optional "+N more" overflow indicator.
  • Separators — draw dividers between items and/or lines (never at the edges); they take part in sizing and wrapping.
  • ✂️ Manual line breaks — force breaks with LineBreak() or .startInNewLine().
  • 🌍 RTL support — adapts to the environment's layout direction.
  • 🧱 Layout conformanceHFlowLayout / VFlowLayout for use anywhere a Layout is expected.

Requirements

  • iOS 16+, macOS 13+, tvOS 16+, watchOS 9+, visionOS 1+
  • Swift 5.9+

Installation

Add the package to your Package.swift:

.package(url: "https://github.com/tevelee/SwiftUI-Flow", from: "3.1.1")

then add "Flow" to your target's dependencies. In Xcode, choose File ▸ Add Package Dependencies… and enter https://github.com/tevelee/SwiftUI-Flow.

Documentation

Full API reference and guides are hosted on the Swift Package Index.

HFlow

import Flow

struct Colors: View {
    let colors: [Color] = [
        .blue,
        .orange,
        .green,
        .yellow,
        .brown,
        .mint,
        .indigo,
        .cyan,
        .gray,
        .pink
    ]

    var body: some View {
        HFlow {
            ForEach(colors, id: \.description) { color in
                RoundedRectangle(cornerRadius: 10)
                    .fill(color.gradient)
                    .frame(width: .random(in: 40...60), height: 50)
            }
        }
        .frame(maxWidth: 300)
    }
}

[HFlow]

VFlow

VFlow {
    ForEach(colors, id: \.description) { color in
        RoundedRectangle(cornerRadius: 10)
            .fill(color.gradient)
            .frame(width: 50, height: .random(in: 40...60))
    }
}
.frame(maxHeight: 300)

[VFlow]

Alignment

Supports the same alignments as HStack and VStack do.

HFlow(alignment: .top) {
    ForEach(colors, id: \.description) { color in
        RoundedRectangle(cornerRadius: 10)
            .fill(color.gradient)
            .frame(width: 50, height: .random(in: 40...60))
    }
}
.frame(maxWidth: 300)

[HFlow]

Additionally, alignment can be specified on both axes. Ideal for tags.

HFlow(horizontalAlignment: .center, verticalAlignment: .top) {
    ForEach(colors, id: \.description) { color in
        RoundedRectangle(cornerRadius: 10)
            .fill(color.gradient)
            .frame(width: .random(in: 30...60), height: 30)
    }
}
.frame(maxWidth: 300)

[HFlow]

Spacing

Customize spacing between rows and items separately.

HFlow(itemSpacing: 4, rowSpacing: 20) {
    ForEach(colors, id: \.description) { color in
        RoundedRectangle(cornerRadius: 10)
            .fill(color.gradient)
            .frame(width: 50, height: 50)
    }
}
.frame(maxWidth: 300)

[HFlow]

Distribute items

Distribute items evenly by minimizing the empty spaces left in each row. Implements the Knuth-Plass line breaking algorithm.

HFlow(distributeItemsEvenly: true) {
    ForEach(colors, id: \.description) { color in
        RoundedRectangle(cornerRadius: 10)
            .fill(color.gradient)
            .frame(width: 65, height: 50)
    }
}
.frame(width: 300, alignment: .leading)
.border(.gray)

[HFlow]

Justified

HFlow(justified: true) {
    ForEach(colors, id: \.description) { color in
        RoundedRectangle(cornerRadius: 10)
            .fill(color.gradient)
            .frame(width: 50, height: 50)
    }
}
.frame(width: 300)

[HFlow]

Flexibility

HFlow { // distributes flexible items proportionally
    RoundedRectangle(cornerRadius: 10)
        .fill(.red)
        .frame(minWidth: 50, maxWidth: .infinity)
        .frame(height: 50)
        .flexibility(.minimum) // takes as little space as possible, rigid
    RoundedRectangle(cornerRadius: 10)
        .fill(.green)
        .frame(minWidth: 50, maxWidth: .infinity)
        .frame(height: 50)
        .flexibility(.natural) // expands
    RoundedRectangle(cornerRadius: 10)
        .fill(.blue)
        .frame(minWidth: 50, maxWidth: .infinity)
        .frame(height: 50)
        .flexibility(.natural) // expands
    RoundedRectangle(cornerRadius: 10)
        .fill(.yellow)
        .frame(minWidth: 50, maxWidth: .infinity)
        .frame(height: 50) // takes as much space as possible
        .flexibility(.maximum)
}
.frame(width: 300)

[HFlow]

Use .grow(_:) to distribute leftover space proportionally by weight:

HFlow {
    RoundedRectangle(cornerRadius: 8).fill(.orange)
        .frame(minWidth: 40, maxWidth: .infinity, minHeight: 44, maxHeight: 44)
        .flexibility(.grow(2))  // takes 2/3 of leftover space

    RoundedRectangle(cornerRadius: 8).fill(.teal)
        .frame(minWidth: 40, maxWidth: .infinity, minHeight: 44, maxHeight: 44)
        .flexibility(.grow(1))  // takes 1/3 of leftover space
}

Line limits

Cap a flow to a maximum number of lines with .maxLines(_:):

HFlow {
    ForEach(tags) { tag in TagView(tag) }
}
.maxLines(2)

Add a trailing overflow indicator using the closure overload (available on HFlow and VFlow):

HFlow {
    ForEach(tags) { tag in TagView(tag) }
}
.maxLines(2) { hidden in
    Text("+\(hidden) more").foregroundStyle(.secondary)
}

[HFlow]

Separators

Draw separators between items and/or lines. They only appear between elements — never at the edges — and they participate in layout: an item separator's width is accounted for when wrapping, and a line separator contributes its height. A gap that wraps onto a new line shows the line separator instead of the item separator.

HFlow {
    ForEach(tags, id: \.self) { Text($0) }
}
.itemSeparator { Text("•").foregroundStyle(.secondary) }
.lineSeparator { Divider() }

The two modifiers are independent; use either one on its own or both together. They are available on HFlow and VFlow.

[HFlow]

Line breaks

HFlow {
    RoundedRectangle(cornerRadius: 10)
        .fill(.red)
        .frame(width: 50, height: 50)
    RoundedRectangle(cornerRadius: 10)
        .fill(.green)
        .frame(width: 50, height: 50)
    RoundedRectangle(cornerRadius: 10)
        .fill(.blue)
        .frame(width: 50, height: 50)
    LineBreak() // <--
    RoundedRectangle(cornerRadius: 10)
        .fill(.yellow)
        .frame(width: 50, height: 50)
}
.frame(width: 300)

[HFlow]

HFlow {
    RoundedRectangle(cornerRadius: 10)
        .fill(.red)
        .frame(width: 50, height: 50)
    RoundedRectangle(cornerRadius: 10)
        .fill(.green)
        .frame(width: 50, height: 50)
        .startInNewLine() // <-- 
    RoundedRectangle(cornerRadius: 10)
        .fill(.blue)
        .frame(width: 50, height: 50)
    RoundedRectangle(cornerRadius: 10)
        .fill(.yellow)
        .frame(width: 50, height: 50)
}
.frame(width: 300)

[HFlow]

RTL

Adapts to left-to-right and right-to-left environments too.

HFlow {
    ForEach(colors, id: \.description) { color in
        RoundedRectangle(cornerRadius: 10)
            .fill(color.gradient)
            .frame(width: .random(in: 40...60), height: 50)
    }
}
.frame(maxWidth: 300)
.environment(\.layoutDirection, .rightToLeft)

[HFlow]

Package Metadata

Repository: tevelee/swiftui-flow

Default branch: main

README: README.md