Several Ways to Center Views in SwiftUI

fatbobman ( 东坡肘子)
9 min readMar 17, 2023

--

Photo by Alexander Grey on Unsplash

Centering a view within its parent view is a common requirement, and it is not difficult even for beginners in SwiftUI. There are many ways to achieve this goal in SwiftUI. This article will introduce some of these methods and explain the implementation principles, applicable scenarios, and precautions for each method.

The original article was written in Chinese and published on my blog 肘子的Swift记事本.

Don’t miss out on the latest updates and excellent articles about Swift, SwiftUI, Core Data, and SwiftData. Subscribe to fatbobman’s Swift Weekly and receive weekly insights and valuable content directly to your inbox.

In light of the fact that my blog, Fatbobman’s Blog, now offers all articles in English, starting from April 1, 2024, I will no longer continue updating articles on Medium. You are cordially invited to visit my blog for more content.

Requirement

Implement the style shown in the figure below: center a single-line Text within a colored rectangle view.

Filler

Spacer

The most common and easiest solution is to use Spacer.

var hello: some View {
Text("Hello world")
.foregroundColor(.white)
.font(.title)
.lineLimit(1)
}

HStack {
Spacer()
hello
Spacer()
}
.frame(width: 300, height: 60)
.background(.blue)

Do you believe there are two hidden dangers in the above code?

  • The text content exceeds the width of the rectangle.

Spacer has a minimum thickness setting, and the default minimum padding thickness is 8px. Even if the text width exceeds the recommended width given by HStack, HStack will still retain its minimum thickness during layout, causing the text at the top of the figure below to not fully utilize the width of the rectangle view.

The solution is to use Spacer(minLength: 0).

Of course, you can also use Spacer to control the width of the Text available in HStack.

  • Placing the synthesized view at the top or bottom of a view that may fill the screen may result in unexpected results.
  VStack {
// Hello world view 1
HStack {
Spacer(minLength: 0)
hello
Spacer(minLength: 0)
}
.frame(width: 300, height: 60)
.background(.blue)

HStack {
Spacer(minLength: 0)
hello
Spacer(minLength: 0)
}
.frame(width: 300, height: 60) // same size
.background(.red)

Spacer() // make VStack fill available space
}

Starting from SwiftUI 3.0, when adding elements that conform to the ShapeStyle protocol using background, you can set the ignoresSafeAreaEdges parameter to determine whether to ignore the safe area. The default value is .all (ignore any safe area). Therefore, when we place the synthesized hello world view at the top of VStack (through Spacer), the background of the rectangle will be rendered along with the top safe area.

The solution is to use .background(.blue, ignoresSafeAreaEdges: []), excluding the desired safe area.

Additionally, in cases where the size of the rectangle is not explicitly set (i.e., when the size is unknown), some adjustments need to be made. For example, when displaying the “hello world” view in a List row and wanting the rectangle to fill the entire row:

List {
HStack {
Spacer(minLength: 0)
hello
Spacer(minLength: 0)
}
.background(.blue)
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0)) // Set the row's insets to 0
}
.listStyle(.plain)
.environment(\.defaultMinListRowHeight, 80) // Set the minimum row height for the list

However, the “hello world” view will not fill the height provided by the row because the HStack's height is determined by the height of its aligned child views, and Spacer only fills horizontally and not vertically (i.e., its height is 0). Therefore, the final required height of the HStack is the same as the height of the Text.

The solution is:

HStack {
Spacer(minLength: 0)
hello
Spacer(minLength: 0)
}
.frame(maxHeight: .infinity) // Fill to suggested height
.background(.blue)

For simplicity, the handling of cases where the size is unknown is omitted in the following text. A fixed size (frame(width: 300, height: 60)) will be used uniformly.

Other Fillers

So, can we use other views to achieve the same filling effect as Spacer? For example:

HStack {
Color.clear
hello
Color.clear
}
.frame(width: 300, height: 60)
.background(Color.cyan)

Unfortunately, with the above code, Text will only be able to use one third of the width of the HStack.

When HStack and VStack perform layout, they provide four different sizing modes (minimum, maximum, explicit size, and unspecified size) for each subview. If the required size of a subview is different under different modes, it means that the view is a resizable view. Then, after specifying the required size of all fixed-size subviews, HStack and VStack will distribute the remaining available size (the proposed size given by the parent view of HStack and VStack minus the required size of fixed-size subviews) evenly (when the priorities are the same) to these resizable views.

Since both Color and Text have the resizable feature, they are divided into thirds by the HStack.

However, we can ensure that Text gets the maximum share by adjusting the view priority, for example:

HStack {
Color.clear
.layoutPriority(0)
hello
.layoutPriority(1)
Color.clear
.layoutPriority(0)
}
.frame(width: 300, height: 60)
.background(Color.cyan)

Text("Hello world,hello world,hello world") // hello is wider than the rectangle

As for why Text still doesn’t use the entire width of HStack in the above picture, it’s because spacing for HStack hasn’t been set explicitly. Set it to 0: HStack(spacing:0).

Setting explicit spacing for the layout container is a good practice. When not explicitly specified, HStack and VStack may have unexpected results during layout. This will also be encountered in the following text.

HStack and VStack do not allocate spacing to Spacer, after all, Spacer represents space occupancy. Therefore, in the first example, even if spacing is not set for HStack, Text will still use the entire width of HStack.

Now that we have learned how to use view priorities, we can also use other views with resizable features as fillers, such as:

  • Rectangle().opacity(0)
  • Color.blue.opacity(0)
  • ContainerRelativeShape().fill(.clear)

In SwiftUI development, Color, Rectangle, and other views are often used to achieve container equal division. Additionally, since Color and Rectangle fill in both dimensions (Spacer fills the dimension of the container it’s in), when used as fillers, they will automatically use all available space (including height), without the need for .frame(maxHeight: .infinity) to handle scenarios where the given size is unclear.

Please read SwiftUI Column #4 Color Is Not Just Color to learn more about Color.

Alignment Guide

In the previous section, we achieved left and right alignment for Text by using padding. For vertical alignment, we utilized the default alignment guide of HStack (.center). In this section, we will use alignment guides to achieve centering.

ZStack

ZStack { // Use the default alignment guide, equivalent to ZStack(alignment:.center)
Color.green
hello
}
.frame(width: 300, height: 60)

The layout logic of the above code is:

  • ZStack provides a recommended size of 300 x 60 for both Color and Text.
  • Color will take the recommended size as its required size (filling the ZStack space).
  • Text has a maximum available width of 300.
  • Color and Text will be aligned according to the center alignment guide (appearing as if Text is centered within Color).

If we modify the code as follows, issues will arise:

ZStack { // When not explicitly setting VStack spacing, inconsistent spacing may occur
Color.gray
.frame(width: 300, height: 60)
hello // The width is not specified, and the text may exceed the width of Color when it is too long
}

The layout logic of the above code is:

  • The size of Color is 300 x 60 (ignoring the recommended size from ZStack).
  • The size of ZStack is the maximum width and height of Color and Text, and it is a variable size (depending on the length of the text).
  • When the recommended width from ZStack is greater than 300, the available width of Text will exceed the width of Color.

Therefore, there are two possible error states:

  • When the text is too long, it may exceed the width of Color.
  • As synthesized views have variable sizes, VStack and HStack may have unexpected spacing allocation when adding spacing (as shown in the figure below). This issue can be resolved by explicitly setting spacing.
VStack { // When not setting spacing explicitly, spacing inconsistency may occur. Explicit setting can solve the problem
ZStack {
Color.green
hello
}
.frame(width: 300, height: 60)

ZStack { // When not explicitly setting VStack spacing, inconsistent spacing may occur
Color.gray
.frame(width: 300, height: 60)
hello // Handling cases where the text exceeds the width of the rectangle is difficult
}

// Spacer version
HStack {
Spacer(minLength: 0)
hello
.sizeInfo()
Spacer(minLength: 0)
.sizeInfo()
}
.frame(width: 300, height: 60)
.background(.blue, ignoresSafeAreaEdges: [])
}

When spacing is not explicitly set for VStack or HStack (i.e., spacing = nil), the layout container will attempt to obtain the preferred spacing value from each subview and apply this value between the adjacent views. As different types of views have different default spacing values, it may appear that the spacing is unevenly distributed (in fact, the layout container correctly follows our requirements). To ensure consistent spacing between all views, you need to set a specific spacing value for the layout container

frame

hello
.frame(width: 300, height: 60) // Default alignment guide is center, equivalent to .frame(width: 300, height: 60, alignment: .center)
.background(.pink)

Layout logic:

  • Use FrameLayout container to layout Text
  • FrameLayout suggests a size of 300 x 60 for Text
  • Align Text with a placeholder view (a blank view with a size of 300 x 600) according to the center alignment guide

This is my favorite way of centering, and it’s also very convenient for dealing with situations where the size is unknown:

hello
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.pink)

To learn more about the implementation of frame, please read the article SwiftUI Layout — Dimensions (Part 2)

overlay

Rectangle() // Color.orange can be used directly
.fill(Color.orange)
.frame(width: 300, height: 60)
.overlay(hello) // Equivalent to .overlay(hello, alignment: .center)

Layout logic:

  • Rectangle will receive a suggested size of 300 x 60 (Rectangle will use the entire size)
  • Use OverlayLayout container to layout Rectangle and Text, using the size required by the main view (the required size of Rectangle)
  • Align Text and Rectangle according to the center alignment guide

Can we achieve a similar style with background? For example:

hello
.background(
Color.cyan.frame(width: 300, height: 60)
)
.border(.red) // Show border to see the layout size of the composed view

Unfortunately, you will get a similar result to the ZStack misuse in the previous section. The text may overflow, and the view cannot obtain the spacing (even if it is explicitly set).

Please read the article SwiftUI Layout — Alignment to learn more about the alignment mechanism of ZStack, overlay, and background.

Geometry

Although it may seem a bit overkill, GeometryReader is a great option when we need to get more information about the views:

GeometryReader { proxy in
hello
.position(.init(x: proxy.size.width / 2, y: proxy.size.height / 2))
.background(Color.brown)
}
.frame(width: 300, height: 60)

Layout logic:

  • GeometryReader will receive a recommended size of 300 x 60.
  • Since GeometryReader has characteristics similar to Color and Rectangle, it will treat the given recommended size as the required size (it will take up all available space).
  • GeometryReader provides a recommended size of 300 x 60 to the Text.
  • The views inside the GeometryReader are aligned based on the topLeading position by default (similar to the effect of overlay(alignment:.topLeading)).
  • Using position, the center point of the Text is aligned with the given position (position is a view modifier that aligns the center point through CGPoint).

Of course, you can also obtain the Geometry information of the Text and center it using offset or padding. However, unless the size of the rectangle is clear, you need to use GeometryReader inside and outside, which can be cumbersome.

Conclusion

This article has selected some representative solutions. As SwiftUI functionality continues to grow, there will be more and more tools available. The fundamentals of SwiftUI layout make it easy to handle changing requirements.

If you found this article helpful or enjoyed reading it, consider making a donation to support my writing. Your contribution will help me continue creating valuable content for you.
Donate via Patreon, Buy Me aCoffee or PayPal.

Want to Connect?

@fatbobman on Twitter.

--

--

fatbobman ( 东坡肘子)

Blogger | Sharing articles at https://fatbobman.com | Publisher of a weekly newsletter on Swift at http://https://weekly.fatbobman.com