SwiftUI Stack Custom Center Alignment

SwiftUI alignment guides give you a high degree of control on how to align subviews within a parent view. I find the most common use is to customise how stack views center their contents.

Here’s the layout I want to create. The text label and the timestamp are both centered in the parent view and the button is off to the right:

Timestamp showing 13:18:14.812 centered in view below Timer label. Red refresh button to the right on a yellow background

Start With Stack Views

As with UIKit layouts, the stack view is a staple of SwiftUI layouts. I’m using a horizontal stack for the timestamp and the button. Placing that in a vertical stack with the label is a good start:

struct ContentView: View {
  @State private var counter = Date()

  var body: some View {
    VStack {
      Text("Timer")
        .font(.title)
            
      HStack(spacing: 16) {
        Text(counter.formatted(.iso8601
          .time(includingFractionalSeconds: true)))
          .font(Font.system(.body, design: .monospaced))
        Button(role: .destructive) {
          counter = Date()
        } label: {
          Image(systemName: "gobackward")
            .imageScale(.large)
        }
        .buttonStyle(.borderedProminent)
      }
    }
  }
}

Here’s how that looks with the vertical stack view frame visible:

Timer label and horizontal stack are center aligned in parent view

By default, the vertical stack view fits and centers its contents and is itself centered in the parent view.

Stack Alignment

A VStack defaults to horizontally aligning its contents by their centers. You can change this default by specifying the alignment (.leading, .center or .trailing):

VStack(alignment: .leading) { ... }

Leading alignment with timer label now on the left

VStack(alignment: .trailing) { ... }

Trailing alignment with timer label on the right

I want the stack view to align the center of the timer text with the center of the timestamp. For that we need a custom alignment.

Custom Alignment Guides

SwiftUI alignment guides define a horizontal or vertical value for a view. We need to give our timestamp text field a custom horizontal alignment that matches its center. We can then tell the vertical stack to use the custom guide when aligning content.

Unfortunately there’s some boilerplate code we need to create a custom alignment. The value of our custom alignment guide needs to conform to the AlignmentID protocol which requires it to have a default value:

struct CustomCenter: AlignmentID {
  static func defaultValue(in context: ViewDimensions) -> CGFloat {
    context[HorizontalAlignment.center]
  }
}

The initializer for the default value takes a width and height (the ViewDimensions) which we can use to calculate the alignment position we want. You can also access any of the standard alignment guides by subscripting the view dimensions. In our case, we default to the center alignment guide.

We can then extend HorizontalAlignment to include our custom center guide:

extension HorizontalAlignment {
  static let customCenter: HorizontalAlignment = .init(CustomCenter.self)
}

Alignment Guide Modifier

The .alignmentGuide modifier sets the vertical (or horizontal) alignment of the view. It provides a closure with the view dimensions for you to compute the alignment offset:

alignmentGuide(_ g: VerticalAlignment,
  computeValue: @escaping (ViewDimensions) -> CGFloat)

This gives us a lot of flexibility. For example, if wanted to align at 25% the width of the text view:

Text(counter.formatted(...))
.alignmentGuide(.customCenter) { context in
  context.width / 4
}

In our case we return the horizontal center alignment:

.alignmentGuide(.customCenter) { context in
  context[HorizontalAlignment.center]
}

Or in more compact form:

.alignmentGuide(.customCenter) {
  $0[HorizontalAlignment.center]
}

Setting the Vertical Stack Alignment

Finally, we tell our vertical stack to use the custom alignment:

VStack(alignment: .customCenter) { ... }

This almost gives us what we want. The timer text and the timestamp text are now centered with each other but our vertical stack is still centered in the parent view. Since the stack fits its content it leaves the timestamp off-center:

Timer text and timestamp centered on each other but offset from the center of the parent view

One way to fix this is tell our timer text view to fill the available width by giving it a flexible frame:

Text("Timer")
  .frame(maxWidth: .infinity)

This also makes the width of the containing stack view expand to fill the available space so that the centers are in the center of the screen:

Timer text and timestamp centered in view

The final view for reference:

struct ContentView: View {
  @State private var counter = Date()

  var body: some View {
    VStack(alignment: .customCenter) {
      Text("Timer")
        .font(.title)
        .frame(maxWidth: .infinity)
            
      HStack(spacing: 16) {
        Text(counter.formatted(.iso8601
          .time(includingFractionalSeconds: true)))
          .font(Font.system(.body, design: .monospaced))
          .alignmentGuide(.customCenter) {
            $0[HorizontalAlignment.center]
          }
        Button(role: .destructive) {
          counter = Date()
        } label: {
          Image(systemName: "gobackward")
            .imageScale(.large)
        }
        .buttonStyle(.borderedProminent)
      }
    }
  }
}