Several Ways to Center Views in SwiftUI
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 bothColor
andText
.Color
will take the recommended size as its required size (filling theZStack
space).Text
has a maximum available width of 300.Color
andText
will be aligned according to the center alignment guide (appearing as ifText
is centered withinColor
).
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 fromZStack
). - The size of
ZStack
is the maximum width and height ofColor
andText
, 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 ofText
will exceed the width ofColor
.
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
andHStack
may have unexpected spacing allocation when addingspacing
(as shown in the figure below). This issue can be resolved by explicitly settingspacing
.
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 forVStack
orHStack
(i.e.,spacing = nil
), the layout container will attempt to obtain the preferredspacing
value from each subview and apply this value between the adjacent views. As different types of views have different defaultspacing
values, it may appear that thespacing
is unevenly distributed (in fact, the layout container correctly follows our requirements). To ensure consistent spacing between all views, you need to set a specificspacing
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.