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:
Value | Behavior |
---|---|
nil | Uses system default spacing values |
Specified value | Acts 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:
Value | Behavior |
---|---|
nil (default) | Expands in both directions when not constrained by a stack |
.horizontal | Expands only horizontally |
.vertical | Expands 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:
Value | Behavior |
---|---|
false (default) | Uses standard spacing logic |
true | Considers 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:
- Expansion: Both horizontal and vertical (when not in a stack)
- Stack behavior: Expands along the stack's major axis
- 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:
- Special feature: Considers text baselines for spacing calculations
- Use case: Vertical layouts with mixed text elements
- 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:
- Expansion: Only horizontal
- Property: Uses
minWidth
instead ofminLength
- 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:
- Expansion: Only vertical
- Property: Uses
minHeight
instead ofminLength
- 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:
- Greedy expansion: Spacers try to take as much space as possible
- Minimum constraints: They respect the
minLength
(or equivalent) property - Axis-specific behavior: Constrained spacers have zero size in the non-expanding axis
- 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
- Use standard
Spacer
for all spacing needs - it's flexible and handles both horizontal and vertical expansion - Specify
minLength
when you need guaranteed minimum spacing - 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:
- Clone the OpenSwiftUI repository via
git clone [email protected]:OpenSwiftUIProject/OpenSwiftUI -b 0.6.0
- Follow the README instructions on Example/README.md and run the hosting example
- Create test layouts using different spacer types
- Observe the sizing behavior in various container types
- 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.