SwiftUI Flow

repository·main·Indexed 20 days ago

https://github.com/tevelee/swiftui-flow

A layout library providing HFlow and VFlow components that allow views to wrap into multiple lines or columns, similar to text wrapping in a paragraph. It features support for the Knuth–Plass algorithm for even item distribution, line justification, flexible item growth via the .flexibility() modifier, and overflow indicators with .maxLines(). The library also includes FlowLayoutScenario for testing layout behaviors through text-based snapshots and property tests.

Tokens
7.9K
Snippets
35
Records
44
Agent score
71%

What's inside swiftui-flow

  1. Overview of SwiftUI Flow Layout

    main

    SwiftUI Flow is a layout system that arranges views in lines, wrapping them onto new lines (for HFlow) or new columns (for VFlow) when they exceed the available space. It behaves similarly to how words wrap in a paragraph.

    Use Flow layouts when you have a variable number of differently sized items—such as tags, chips, filters, or thumbnails—that need to fill the available width or height and wrap as needed. Because it is built on the SwiftUI Layout protocol, it integrates with standard SwiftUI features like alignment guides, layout priorities, spacing preferences, and animations.

  2. Use HFlow and VFlow for wrapping layouts

    main

    The core components of the library are HFlow and VFlow:

    • HFlow: Works like an HStack but wraps content onto additional rows when it runs out of horizontal space.
    • VFlow: Works like a VStack but wraps content onto additional columns when it runs out of vertical space.

    These components are the primary way to implement wrapping behavior in your SwiftUI views.

  3. How HFlow adapts to layout direction

    main

    Both HFlow and VFlow automatically respect the environment's layoutDirection. In a right-to-left (RTL) environment, HFlow will start arranging items from the trailing edge instead of the leading edge.

    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)
  4. Use property tests for layout invariants

    main

    Property tests should be used to verify invariants across many configurations rather than exact geometry.

    Key Invariants to Test:

    • Geometry: Finite placements, non-negative sizes, and containment.
    • Ordering: Visible traversal order.
    • Spacing: Equivalence of nil spacing and .zero subview spacing.
    • EdgeCases: Fractional geometry and non-overlap policies.
    • Metamorphic: H/V transpose and monotonic proposal checks.

    Best Practices:

    • Group tests by behavior (e.g., Geometry), not by generator implementation.
    • Build fresh TestSubview instances inside each iteration to avoid reusing mutable placement state.
    • Do not commit .fixedSeed(...) traits to the repository. Use them locally to reproduce and shrink failures, then convert the failure into a named requirement test.
  5. Use text snapshots for layout verification

    main

    The library uses text-based snapshots to verify layout results.

    • assertLayoutRendering: The default method. It renders subviews with stable labels (A, B, C, etc.). Overlaps are represented by *, and cells outside the reported size are clipped.
    • assertLayoutTranscript: Use this when the visual grid is insufficient to capture details like zero-size line-break subviews, fractional origins (e.g., 0.5), or negative spacing.

    To record or update a failed text snapshot, use the SNAPSHOT_TESTING_RECORD=failed environment variable:

    SNAPSHOT_TESTING_RECORD=failed swift test --filter <TestClassName>
    swift test --filter <TestClassName>
    SNAPSHOT_TESTING_RECORD=failed swift test --filter FlowLineBreakRequirementTests
    swift test --filter FlowLineBreakRequirementTests
  6. Justify lines in HFlow

    main

    To make each line fill the full available width (or height) by aligning both edges, set the justified parameter to true. This stretches the spacing between items within each line so that the edges of the line touch the boundaries of the container.

    HFlow(justified: true) {
        ForEach(colors, id: \.description) { color in
            RoundedRectangle(cornerRadius: 10)
                .fill(color.gradient)
                .frame(width: 50, height: 50)
        }
    }
    .frame(width: 300)
  7. Naming conventions for Flow layout tests

    main

    To maintain clarity in the test suite, follow these naming conventions:

    • Orientation Prefixes: Use HFlow_ for horizontal layout tests and VFlow_ for vertical layout tests.
    • Behavioral Statements: Name tests as descriptions of the behavior being verified (e.g., HFlow_lineBreakMarker_forcesNewRow or VFlow_negativeSpacing_reducesColumnHeight).
  8. Insert a line break using LineBreak

    main

    To force the next item in a flow layout to start on a fresh line, insert the LineBreak() view between your items. This is useful when you want to manually control wrapping instead of relying on the layout's automatic calculation.

    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() // <-- Forces the next item to a new line
        RoundedRectangle(cornerRadius: 10)
            .fill(.yellow)
            .frame(width: 50, height: 50)
    }
    .frame(width: 300)
  9. Configure spacing in HFlow and VFlow

    main

    You can control the space between individual items and the space between rows or columns in HFlow and VFlow.

    There are two ways to configure spacing:

    1. Unified Spacing: Pass a single spacing: value to set both item and row/column spacing to the same value.
    2. Independent Spacing: Set itemSpacing and rowSpacing (for HFlow) or columnSpacing (for VFlow) separately to have fine-grained control.

    If no spacing is provided, the Flow layout defaults to each view's preferred spacing, behaving similarly to standard SwiftUI HStack and VStack.

    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)
  10. Distribute items evenly in HFlow

    main

    By default, a flow layout greedily fills each line before wrapping, which can result in a sparse last line. To balance items across all lines and minimize empty space, set distributeItemsEvenly to true. This uses the Knuth–Plass line breaking algorithm to distribute items more naturally across the available space.

    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)