2025年7月12日

|

Everything You Should Know About Spacer

Spacers are fundamental building blocks in SwiftUI that create flexible spaces in your layouts.

While most developers are familiar with the basic Spacer, SwiftUI actually provides four different spacer types, each designed for specific use cases.

Let's dive deep into understanding these spacer types and their implementations based on the OpenSwiftUI project.

The Foundation: PrimitiveSpacer Protocol

All spacer types in SwiftUI conform to the PrimitiveSpacer protocol, which defines three key requirements:

private protocol PrimitiveSpacer: PrimitiveView, UnaryView {
    var minLength: CGFloat? { get }
    static var axis: Axis? { get }
    static var requireTextBaselineSpacing: Bool { get }
}

Let's examine each requirement in detail:

1. minLength: The Common Foundation

The minLength property is the shared characteristic among all spacer types. It defines the minimum size the spacer can be compressed to along its expansion axis:

ValueBehavior
nilUses system default spacing values
Specified valueActs as a minimum constraint for the spacer's size

This property ensures that spacers maintain a minimum visual separation even when space is constrained.

2. axis: Directional Constraints

The axis property determines the spacer's expansion behavior:

ValueBehavior
nil
(default)
Expands in both directions when not constrained by a stack
.horizontalExpands only horizontally
.verticalExpands only vertically

This property is crucial for the spacer's sizing logic and determines how it interacts with its container.

3. requireTextBaselineSpacing: Typography-Aware Layouts

This boolean property enables special baseline-aware spacing calculations:

ValueBehavior
false
(default)
Uses standard spacing logic
trueConsiders text baselines for more precise typography layouts

This is particularly important for layouts that need to align text elements properly.

The Four Spacer Types

1. Spacer - The Universal Expander

@frozen
public struct Spacer: PrimitiveSpacer {
    public var minLength: CGFloat?
    
    // Default implementations:
    // static var axis: Axis? { nil }
    // static var requireTextBaselineSpacing: Bool { false }
}

Key characteristics:

  1. Expansion: Both horizontal and vertical (when not in a stack)
  2. Stack behavior: Expands along the stack's major axis
  3. Baseline awareness: No special text baseline handling

Usage example:

HStack {
    Text("Left")
    Spacer()  // Expands horizontally in HStack
    Text("Right")
}

2. _TextBaselineRelativeSpacer - The Typography Expert

@frozen
public struct _TextBaselineRelativeSpacer: PrimitiveSpacer {
    public var minLength: CGFloat?
    
    static var requireTextBaselineSpacing: Bool { true }
}

Key characteristics:

  1. Special feature: Considers text baselines for spacing calculations
  2. Use case: Vertical layouts with mixed text elements
  3. Baseline awareness: Adjusts spacing based on first and last baselines of adjacent text

Usage example:

VStack {
    Text("Title").font(.largeTitle)
    _TextBaselineRelativeSpacer()  // Adjusts based on text baselines
    Text("Subtitle").font(.caption)
}

3. _HSpacer - The Horizontal Specialist

@frozen
public struct _HSpacer: PrimitiveSpacer {
    public var minWidth: CGFloat?
    
    var minLength: CGFloat? { minWidth }
    static var axis: Axis? { .horizontal }
}

Key characteristics:

  1. Expansion: Only horizontal
  2. Property: Uses minWidth instead of minLength
  3. Constraint: Never expands vertically

Usage example:

VStack {
    HStack {
        Text("Start")
        _HSpacer(minWidth: 50)  // Minimum 50pt horizontal space
        Text("End")
    }
}

4. _VSpacer - The Vertical Specialist

@frozen
public struct _VSpacer: PrimitiveSpacer {
    public var minHeight: CGFloat?
    
    var minLength: CGFloat? { minHeight }
    static var axis: Axis? { .vertical }
}

Key characteristics:

  1. Expansion: Only vertical
  2. Property: Uses minHeight instead of minLength
  3. Constraint: Never expands horizontally

Usage example:

HStack {
    VStack {
        Text("Top")
        _VSpacer(minHeight: 30)  // Minimum 30pt vertical space
        Text("Bottom")
    }
}

Internal Implementation: Sizing Logic

The sizeThatFits logic in the SpacerLayoutComputer reveals how each spacer calculates its size:

mutating func sizeThatFits(_ proposedSize: _ProposedSize) -> CGSize {
    let value = spacer.minLength ?? defaultSpacingValue[orientation ?? .horizontal]
    return switch orientation {
    case .horizontal:
        CGSize(
            width: max(proposedSize.width ?? -.infinity, value),
            height: 0
        )
    case .vertical:
        CGSize(
            width: 0,
            height: max(proposedSize.height ?? -.infinity, value)
        )
    case nil:
        CGSize(
            width: max(proposedSize.width ?? -.infinity, value),
            height: max(proposedSize.height ?? -.infinity, value)
        )
    }
}

Key insights:

  1. Greedy expansion: Spacers try to take as much space as possible
  2. Minimum constraints: They respect the minLength (or equivalent) property
  3. Axis-specific behavior: Constrained spacers have zero size in the non-expanding axis
  4. Default values: System spacing is used when no minimum is specified

Spacing Logic Analysis

The spacing logic differs between baseline-aware and standard spacers:

Standard Spacers (Spacer, HSpacer, VSpacer)

// Horizontal spacer spacing
if orientation == .horizontal {
    return .horizontal(.zero)
} else {
    return .vertical(.zero)
}

Baseline-Aware Spacers (_TextBaselineRelativeSpacer)

// Horizontal baseline spacing
return .init(minima: [
    .init(category: .leftTextBaseline, edge: .left): .distance(0),
    .init(category: .rightTextBaseline, edge: .right): .distance(0),
    // ... additional baseline categories
])

// Vertical baseline spacing
return .init(minima: [
    .init(category: .textBaseline, edge: .top): .distance(0),
    .init(category: .textBaseline, edge: .bottom): .distance(0),
    // ... additional baseline categories
])

The baseline-aware spacers provide more sophisticated spacing calculations that consider text metrics.

Usage Recommendations

Understanding Private APIs

While _HSpacer and _VSpacer exist in SwiftUI's implementation, they are private APIs (indicated by the underscore prefix) and should not be used in production code. These types are useful for understanding how SwiftUI's layout system works internally, but you should rely on the public Spacer API for your applications.

Working with the Public Spacer API

The standard Spacer is designed to handle all common spacing scenarios effectively:

// Horizontal spacing in VStack
VStack {
    HStack {
        Text("Label:")
        Spacer(minLength: 20)  // Same effect as _HSpacer(minWidth: 20)
        Text("Value")
    }
    VStack {
        Text("Title")
        Spacer(minLength: 12)  // Same effect as _VSpacer(minHeight: 12)
        Text("Content")
    }
}

Best Practices

  1. Use standard Spacer for all spacing needs - it's flexible and handles both horizontal and vertical expansion
  2. Specify minLength when you need guaranteed minimum spacing
  3. Consider _TextBaselineRelativeSpacer for text-heavy layouts requiring precise typography (though this is also a private API)

Exploring with OpenSwiftUI

The OpenSwiftUI project provides an excellent opportunity to understand these spacer implementations in detail. You can explore the complete implementation at Spacer.

To experiment with these spacers:

  1. Clone the OpenSwiftUI repository via

git clone [email protected]:OpenSwiftUIProject/OpenSwiftUI -b 0.6.0

  1. Follow the README instructions on Example/README.md and run the hosting example
  2. Create test layouts using different spacer types
  3. Observe the sizing behavior in various container types
  4. Experiment with different minLength values

Alternatively, you can explore the UI test cases at SpacerUITests.swift.

Conclusion

Understanding the four spacer types in SwiftUI—Spacer, _TextBaselineRelativeSpacer, _HSpacer, and _VSpacer—gives you valuable insights into how SwiftUI's layout system works internally. However, for production applications, you should focus on using the public Spacer API, which provides:

  • Flexible expansion: Adapts to container constraints automatically
  • Minimum length control: Ensures consistent spacing with minLength parameter

The private spacer types (_HSpacer, _VSpacer, _TextBaselineRelativeSpacer) are valuable for understanding SwiftUI's internal architecture and can inform your decisions about how to use the public Spacer API effectively. By understanding their implementation through the PrimitiveSpacer protocol, you gain deeper insight into SwiftUI's layout system without relying on unstable private APIs.

Remember that the public Spacer API is designed to handle all common spacing scenarios. When you need axis-specific behavior, you can achieve it through proper container usage (like placing spacers in HStack for horizontal expansion or VStack for vertical expansion) rather than relying on private APIs.