In this part you will learn about the Stack container views included with SwiftUI and explain how you can use then to create user interface laying out all the componenrs according to your needs.
Table of contents
As you saw in previous part, SwiftUI includes a wide range of user interface components you can use to develop your app such as labels, buttons or sliders.
The true art of making user interface is a matter of selecting the right interface components, deciding how they will be positioned on the screen, and planning resonable navigation between all screens and views of your app. All good framework provides a set of components dedicated to control how other components are laid out. With them you define some kind of rules how the user interface is organized and the way in which the layout responds to changes in screen orientation and size.
SwiftUI includes three stack layout views in the form of VStack
(for vertical laying out), HStack
(for horizontal) and ZStack
(controling how views are layered on top of each other).
VStack
and HStack
behaves very similar way and differs only in orientation – you saw their usage in previous part related to user interface controls. ZStack
controls "depth" – you can put controls or elements on the top of other user interface components.
Every stack is declared by embedding child views into a stack view:
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { var body: some View { HStack { Image(systemName: "play") Image(systemName: "pause") Image(systemName: "stop") } } } |
To embed an existing component into a stack, either wrap it manually within a stack declaration as you did above, or hover the mouse pointer over the component in the editor so that it highlights, hold down the Command key on the keyboard and left-click on the component and from the resulting popup menu select the appropriate option:
You can realize more complex layouts simply by embedding stacks within other stacks, for example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
struct ContentView: View { var body: some View { VStack { Text("Color table") .font(.title) HStack { VStack{ Text("Color") .font(.headline) Text("red") .foregroundColor(.red) Text("green") .foregroundColor(.green) Text("blue") .foregroundColor(.blue) Text("yellow") .foregroundColor( Color( red: 255.0/255.0, green: 255.0/255.0, blue: 0.0 ) ) } VStack{ Text("RGB") Text("(255,0,0)") Text("(0,255,0)") Text("(0,0,255)") Text("(255,255,0)") } VStack{ Text("Hex") .font(.headline) Text("FF0000") .font(.system(.body, design: .monospaced)) Text("00FF00") .font(.system(.body, design: .monospaced)) Text("0000FF") .font(.system(.body, design: .monospaced)) Text("FFFF00") .font(.system(.body, design: .monospaced)) } } } } } |
It's not bad however far from perfection. Notice for example what will happend if you replace
1 2 |
Text("blue") .foregroundColor(.blue) |
with
1 2 |
Text("blue\ncolor") .foregroundColor(.blue) |
This happen because every VStack
is independent from each other, so there is no row height synchronization. As you can see your layout needs some additional work, particularly in terms of alignment and spacing. You can do this using a combination of the spacer component, alignment settings and the padding modifier.
Alignment is super easy and it's hard to say something really new in this context. Simply watch the examples below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct ContentView: View { var body: some View { HStack { VStack(alignment: .leading){ Text("XXX") Text("X") }.foregroundColor(.red) VStack(alignment: .center){ Text("XXX") Text("X") }.foregroundColor(.green) VStack(alignment: .trailing){ Text("XXX") Text("X") }.foregroundColor(.blue) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct ContentView: View { var body: some View { HStack { HStack(alignment: .top){ Text("XXX\nXXX") Text("XXX") }.foregroundColor(.red) HStack(alignment: .center){ Text("XXX\nXXX") Text("XXX") }.foregroundColor(.green) HStack(alignment: .bottom){ Text("XXX\nXXX") Text("XXX") }.foregroundColor(.blue) } } } |
To move the elements of your user interface apart, you need to add space between them. This space shouldn't be fixed but rather flexibly expand and contract along the axis of the containing stack (either horizontally or vertically) to provide a separating area between views. For this purpose, in SwiftUI you use the
Spacer
component.
SwiftUI’s Spacer view automatically fill up all available space on their axis of expansion, which means that it take up as much space as it can either horizontally or vertically, depending on what you put them in:
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { HStack { Image(systemName: "1.circle") Spacer() Image(systemName: "2.circle") Image(systemName: "3.circle") }.font(.largeTitle) } } |
1 2 3 4 5 6 7 8 9 10 11 |
struct ContentView: View { var body: some View { HStack { Image(systemName: "1.circle") Spacer() Image(systemName: "2.circle") Spacer() Image(systemName: "3.circle") }.font(.largeTitle) } } |
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Spacer() Image(systemName: "2.circle") Image(systemName: "3.circle") }.font(.largeTitle) } } |
1 2 3 4 5 6 7 8 9 10 11 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Spacer() Image(systemName: "2.circle") Spacer() Image(systemName: "3.circle") }.font(.largeTitle) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
struct ContentView: View { var body: some View { VStack { HStack { Image(systemName: "1.circle.fill") Image(systemName: "2.circle.fill") } HStack { Image(systemName: "3.circle.fill") Image(systemName: "4.circle.fill") Spacer() } HStack { Image(systemName: "5.circle.fill") Spacer() Image(systemName: "6.circle.fill") } Spacer() HStack { Image(systemName: "arrow.left.circle.fill") Spacer() Image(systemName: "arrow.clockwise.circle.fill") Spacer() Image(systemName: "arrow.right.circle.fill") } Spacer() HStack { Image(systemName: "1.circle.fill") } HStack { Spacer() Image(systemName: "1.circle.fill") } } .foregroundColor(.white) .background(Rectangle()) .foregroundColor(.yellow) .font(.largeTitle) } } |
If you replace:
1 2 3 4 |
.foregroundColor(.white) .background(Rectangle()) .foregroundColor(.yellow) .font(.largeTitle) |
with:
1 2 3 |
.foregroundColor(.white) .background(.yellow) .font(.largeTitle) |
you will get:
As you may notice
Spacer
divide available space into equidistant separating area. In most cases this is what you want but sometimes you may keep more control over this process. You can do sophisticated calculations which is out of this material scope. The simples solution is to modify Spacer
with frame.
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Spacer() Image(systemName: "2.circle") } .font(.largeTitle) } } |
1 2 3 4 5 6 7 8 9 10 11 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Spacer() .frame(height: 50) Image(systemName: "2.circle") } .font(.largeTitle) } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Spacer() .frame(height: 50) Image(systemName: "2.circle") Spacer() } .font(.largeTitle) } } |
Frame defined above is very restrictive – it must have height of 50 points. Better idea is to give system some flexibility to take up as much space as it can, however restricted to some extremal values: minimum i maximum. You can do this for example using .frame(minHeight: 50, maxHeight: 500)
or specifying some constraints regardless of direction with for example Spacer(minLength: 50)
.
Another option to add spacing around the sides of any view is by using the
padding()
modifier. When called without a parameter system automatically use the "best" padding for the layout, content and screen size.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Image(systemName: "2.circle") Image(systemName: "3.circle") .padding() Image(systemName: "4.circle") Image(systemName: "5.circle") } .font(.largeTitle) } } |
You may also pass a specific amount of padding as a parameter to the modifier:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Image(systemName: "2.circle") Image(systemName: "3.circle") .padding(100) Image(systemName: "4.circle") Image(systemName: "5.circle") } .font(.largeTitle) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct ContentView: View { var body: some View { VStack { Image(systemName: "1.circle") Image(systemName: "2.circle") Image(systemName: "3.circle") .padding(.top, 50) .padding(.bottom, 100) Image(systemName: "4.circle") Image(systemName: "5.circle") } .font(.largeTitle) } } |
Group
viewCreating a user interface one day you may be surprised by a message
1 |
Extra argument in call |
and your code will not compile:
However strange it may sound, the reason is very simple to explain. All container views in SwiftUI are limited to 10 direct descendant views. If a stack contains more than 10 direct children, the views will need to be sprad among multiple containers – you will have to add more grouping intermediate components for example by adding stacks as subviews. Don't affraid, performance of your app will not be affected by increasing the deepth of hierarchy.
Container deicated to this purpose is the Group
view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
struct ContentView: View { var body: some View { VStack { Group { Text("line 1") Text("line 2") Text("line 3") Text("line 4") Text("line 5") } Group { Text("line 6") Text("line 7") Text("line 8") Text("line 9") Text("line 10") Text("line 11") } } .font(.largeTitle) } } |
A you can see Group
doesn't introduce any disturbance in components layout.
By default, an HStack
attempt to display the text within its Text view children on a single line. If there is insufficient room the text will automatically wrap onto multiple lines when necessary if it is not restricted by the lineCount()
modifier:
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { var body: some View { HStack { Text("abc def ghi jkl mno pqr") Text("012 345 678 9") } .font(.largeTitle) } } |
If modifier is applied, the stack view will decide how to truncate the text based on the available space and the length of the views:
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { var body: some View { HStack { Text("abc def ghi jkl mno pqr") Text("012 345 678 9") } .font(.largeTitle) .lineLimit(1) } } |
You can send to the stack view informations about your preferences – you as a user interfce designer have a better understanding which text can be truncated and which should stay unaffected as long as possible. You do this by adding layoutPriority()
modifier to the views in the stack and passing values indicating the level of priority for the corresponding view. The higher the number, the greater the layout priority and the less the view will be subjected to truncation.
1 2 3 4 5 6 7 8 9 10 11 |
struct ContentView: View { var body: some View { HStack { Text("abc def ghi jkl mno pqr") Text("012 345 678 9") .layoutPriority(1) } .font(.largeTitle) .lineLimit(1) } } |
ZStack
viewZStack
adds depth to your layout. With this container you can place component on the top of other components.
One of the simplest usage example of ZStack
is to overlap a text label on top of an image:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct ContentView: View { var body: some View { ZStack { Image(systemName: "bubble.left") .resizable() .frame( width: 200.0, height: 200.0 ) Text("SwiftUI").font(.title).fontWeight(.bold) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct ContentView: View { var body: some View { ZStack { LinearGradient( colors: [.yellow, .orange], startPoint: .top, endPoint: .bottom ) .ignoresSafeArea() Text("SwiftUI") .foregroundColor(.white) .font(.title) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
struct ContentView: View { var screenSize = UIScreen.main.bounds var body: some View { ZStack { LinearGradient( colors: [.orange, .red], startPoint: .top, endPoint: .bottom ) .ignoresSafeArea() VStack(spacing: 150) { Spacer() Image(systemName: "person.circle") .resizable() .frame(width: 200, height: 200) VStack(spacing: 20) { Button(action: { print("Login...") }) { Text("Login") .foregroundColor(.black) .font(.system(size: 22)) }.frame(width: screenSize.width - 40, height: 50) .background(Color.yellow) .cornerRadius(5) Button(action: { print("Sign in") }) { Text("Sign in") .foregroundColor(.black) .font(.system(size: 22)) }.frame(width: screenSize.width - 40, height: 50) .background(Color.yellow) .cornerRadius(5) } Spacer() } } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
struct ContentView: View { var body: some View { ZStack { VStack(spacing: 0) { Color.yellow Color.red } Text("SwiftUI") .foregroundColor(.white) .padding(50) .background(.orange) } .ignoresSafeArea() } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
struct ContentView: View { var body: some View { ZStack { LinearGradient(gradient: Gradient(colors: [Color.yellow, Color.orange]), startPoint: .topLeading, endPoint: .bottomTrailing) HStack { Image(systemName: "swift") .foregroundColor(.white) .font(.system(size: 35)) Text("SwiftUI") .font(.largeTitle) .foregroundColor(.white) .fontWeight(.bold) } .padding() .background(.red) .cornerRadius(10) .shadow(color: .white, radius: 20) }.ignoresSafeArea() } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
struct ContentView: View { var body: some View { ZStack { LinearGradient(gradient: Gradient(colors: [Color.yellow, Color.orange]), startPoint: .topLeading, endPoint: .bottomTrailing) HStack { Image(systemName: "swift") .foregroundColor(.white) .font(.system(size: 15)) .offset(x: -10, y: -20) Text("SwiftUI") .font(.largeTitle) .foregroundColor(.white) .fontWeight(.bold) .offset(x: 10, y: 15) } .padding() .background(.red) .cornerRadius(10) .shadow(color: .white, radius: 20) .offset(x: 50, y: 150) }.ignoresSafeArea() } } |