Back to blog

Create UIs with Views and Modifiers in SwiftUI


Kasib Khan
By Kasib Khan | Last Updated on May 30th, 2023 11:51 am | 5-min read

How do you create User Interfaces with SwiftUI? In this tutorial, we’ll dive into the fundamental concepts and approaches that make SwiftUI work. We’ll discuss views and modifiers, and create a card view in the process.

Here’s what we’ll get into:

  • How SwiftUI’s syntax and composition works
  • What views and modifiers are, and how they work
  • How to work with VStack, HStack, Text, Spacer and Image views
  • Using modifiers like frame(), font(), padding() and shadow()
  • Tips and tricks to work productively with SwiftUI in Xcode
  • Customizing SwiftUI views with code, as well as SwiftUI Inspector
  • Working with Live Preview in Xcode
  • How SwiftUI uses closures and generics to enable a concise syntax

Create a Card View UI with SwiftUI

We’re going to start building the Card View app by setting up a new project in Xcode. Simply choose the Single View App, make sure to choose SwiftUI for User Interface, and save the project in a convenient folder on your Mac.

In the new project, open ContentView.swift. You now see the following SwiftUI code:

Before moving on, feel free to rename the ContentView struct to CardView. You can do this by right-clicking on ContentView in struct ContentView: View { and then choose Refactor → Rename. In the UI that appears, change the name of the view, and make sure that the name is changed throughout the project.

Awesome! Let’s get a move on with the first view in our Card View: Text. Here’s what you do:

  1. Find the Text view inside the CardView struct
  2. Change its text, i.e. the string, from Hello world! to your own name
  3. Check out your changes in the Preview on the right of Xcode

This is what the view looks like in code right now:

struct CardView: View {
var body: some View {
Text(“Reinder de Vries”)
}
}

If your view didn’t have a Text view to begin with, simply add it yourself right within the body property. Make sure your code reflects the above example.

The Preview in Xcode updates automatically as you make changes, which is really neat. It works for almost every visual change you make in your SwiftUI views, which is an incredibly productive way to build views with SwiftUI.

As you’ve guessed, the Text view in SwiftUI displays… text. You can change its font, color, size, padding, and a whole lot more. The Text view in SwiftUI is the defacto component to display textual strings, much like UILabel for UIKit.

Next up, let’s add two more views to the CardView. Add the following code on the lines below the previous Text element:

Text(“iOS Developer”)
Text(“@reinder42”)

Feel free to customize this view with your own name, job title and Twitter username.

What’s going on now? It looks like we’ve got an error in Xcode! Here’s what it says:

  • Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type
  • Result of ‘Text’ initializer is unused

Hmm. This is a good opportunity to tell you: SwiftUI error messages in Xcode don’t always make sense. As for the above error messages, SwiftUI thinks we’ve made a mistake with the return type for CardView, and it’s also confused about what exactly we’re “returning” from the view.

Let’s fix that next.

Build Views with Stacks

The real problem right here is that we’ve added 3 subviews to a View that’s only supposed to have one subview; one return value.

Here’s how we’re going to fix that. Change your code to reflect the following:

struct CardView: View {
var body: some View {
VStack {
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}
}
}

We’ve wrapped the 3 Text views in to a VStack, a vertical stack view. Each of the text views now shows up above the other, in a neat vertical stack. Stack views are centered by default, which you can see in the Preview on the right of Xcode.

As its name implies, a Stack displays multiple views on top of each other. In SwiftUI, we’ve got 3 types of stacks: the VStack for vertical stacking, the HStack for horizontal stacking, and ZStack for stacking by 3D depth.

Here’s what we got so far:

Next up, we’re going to add another stack: the HStack, or horizontal stack. We’ll use it to add an image to the right of the text views we’ve added.

Here’s how:

  1. While holding the Command key, click the VStack view
  2. Click Embed in HStack

This will wrap the VStack, that we created before, in a new HStack. We’re wrapping views in views, creating elaborate structures, which is what SwiftUI is all about!

This is the body of the view we’ve coded so far:

HStack {
VStack {
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}
}

Here’s what you do next:

  1. Add an empty line right below the closing bracket } of the VStack
  2. Press Command + Shift + L or click the + button in the top-right of Xcode
  3. The Library shows up now; Make sure you’ve selected the Views library, the left-most tab in the UI
  4. In the search field, start typing: image
  5. Double-click the Image view when it appears in the list, which inserts an Image() view into the CardView at the cursor position

Finally, import an image into your project’s asset catalog in Xcode. Doesn’t matter what it is, as long as it’s square and about 75 × 75 points. Change the name of the Image() view into the name of the asset you imported.

You’ll end up with the following code:

struct CardView: View {
var body: some View {
HStack {
VStack {
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}
Image(“reinder”)
}
}
}

Let’s do a quick recap.

We started with just one Text view inside the CardView body. Then, we added 2 more Text views to the view – for 3 bits of text total. Because a SwiftUI view can technically only have one subview, we wrapped the Text views in a vertical stack: the VStack. This shows the text views vertically on top of each other.

We then wanted to add an image to the right of those text items. We embeded the VStack into a HStack. We added an Image view directly into the HStack, which stacks the VStack and Image views horizontally next to each other. Neat!

Next up, we’re going to adjust the views with modifiers.

You can use the Library with Command + Shift + L to insert SwiftUI views into your code, or you can just code them yourself. You can even edit your UI with the SwiftUI Inspector, which we’ll discuss next.

Configure Views with Modifiers

Building views with SwiftUI consists of 2 parts. We’ve discussed how you build views like the CardView with other views – subviews – that you compose to create a User Interface. The second step in building UIs happens with modifiers that allow you to configure views, in all sorts of ways.

So far we’ve created the following CardView:

HStack {
VStack {
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}
Image(“reinder”)
}

In the next few sections, we’re going to adjust the view’s text, color, position, shadow, and corner radius. We’ll also discuss a few of the approaches you can use to configure views with Xcode and SwiftUI. Let’s move on!

Changing the Text Views

First, we’re changing the top Text view – the one with your name. Change the Text() code to this:

Text(“Reinder de Vries”)
.font(.system(size: 25))
.fontWeight(.semibold)

It’s straightforward to infer that the above code changes the Font of the text view to a system’s font, size 25, and it’s Font Weight is set to semibold.

Let’s take a closer look at the syntax. In SwiftUI, you can use modifiers to change the attributes of a view. In practical terms, modifiers are Swift functions that take input parameters and return new views. You can chain them together, like we’ve done.

Here, check this out. It’s the same code as before, but written on one line.

Text(“Reinder de Vries”).font(.system(size: 25)).fontWeight(.semibold)

Reminds you a bit of Swift functions, right? It’s the same dot-style syntax you’ve used with “regular” Swift. The fontWeight(_:) function takes a parameter, .semibold, which is an enum of type Font.Weight, for example.

Take a look at this approach:

var text = Text(“Reinder de Vries”)
text = text.font(.system(size: 25))
text = text.fontWeight(.semibold)

In the above code, we’ve deconstructed the chained SwiftUI syntax and assigned the Text view to a variable named text. Instead of chaining all modifiers together, we’re calling the font(_:) and fontWeight(_:) functions on text and assign the resulting view right back to text again. Each call to a modifier returns a View.

This approach is called the Builder Pattern, which is fundamental to SwiftUI’s syntax. You’re essentially “building” up a view, one modifier at a time, constantly creating new views in the process. The chained syntax is of course the most concise. The indentation helps you read the code more easily.

OK, back to the code. Here’s what we got so far:

HStack {
VStack {
Text(“Reinder de Vries”)
.font(.system(size: 25))
.fontWeight(.semibold)
Text(“iOS Developer”)
Text(“@reinder42”)
}
Image(“reinder”)
}

Adjust the 2 other Text views to this:

Text(“iOS Developer”)
.font(.body)
.fontWeight(.medium)
Text(“@reinder42”)
.font(.body)
.foregroundColor(Color(#colorLiteral(red: 0.3333333433, green: 0.3333333433, blue: 0.3333333433, alpha: 1)))

That #colorLiteral code is a color literal, a literal value for a color that you can add and edit directly in Xcode. It’s super useful! Here’s how you can insert your own colors in Xcode’s editor:

  1. (Remove the color literal from your code)
  2. Make sure the cursor is placed inside the parentheses of Color()
  3. Press the Esc key (or your code completion trigger key)
  4. Start typing Color Literal, then select that item from the dropdown list
  5. In the color gizmo that appears, select your favorite color (if the color picker doesn’t show, double-click the color literal/square itself)

Changing the VStack

Awesome! Next up, we’re going to change the VStack view. Right now, the stack views are centered. This is their default state. We want to align the text to the left and add some vertical spacing between them.

As we’ve discussed before, modifiers change the attributes of SwiftUI views. You can modify a view with a function, like font(), but you can also use the view’s initializer directly.

Pick one of the following 2 approaches:

  1. Adjust the VStack with code:
    1. Locate the VStack; inside the top-level HStack view of CardView
    2. Change its code to VStack(alignment: .leading, spacing: 2) {
  2. Adjust the VStack with SwiftUI Inspector:
    1. Locate the VStack; inside the top-level HStack view of CardView
    2. Hold the Command key and click the VStack view
    3. Choose Show SwiftUI Inspector in the menu that pops out
    4. Set spacing to 2 and alignment to Left (or “Leading”)

See what’s going on there? You can adjust views in SwiftUI with code, as well as use the SwiftUI Inspector. If you don’t exactly know the modifiers you’re looking for, the SwiftUI Inspector is an effective way to customize your views.

Quick Tip: You can also show the SwiftUI Inspector by holding Option + Command while clicking on a SwiftUI view. You can do this in the Live Preview area as well.

Working with Spacer

Right now, the text views and the images are a bit too close together. It’s better to give the UI more space, so it “breathes” some more.

Here’s how you do that:

  1. Locate the closing bracket of the VStack, and the Image view
  2. Add a new line between them; code Spacer() in there

You’ve got this code now:

HStack {
VStack(alignment: .leading, spacing: 2) {

}
Spacer()
Image(“reinder”)
}

See how the HStack has 3 subviews now? A VStack, a Spacer and an Image. The spacer is creating space between the VStack and the image, pressing them outwards. Using spacers a great way to distribute views evenly. We’ll add some constraints next, to limit the width of the card view.

Changing the HStack

The last thing we’ll change about the CardView is the HStack. We’re going to add a few modifiers that’ll change its layout.

First, locate the closing bracket of the HStack in the CardView. Add the following code below it:

.frame(maxWidth: 300)
.padding()
.background(Color.white)
.cornerRadius(20)
.shadow(radius: 20)

You’ll end up with the following code:

HStack {

}
.frame(maxWidth: 300)
.padding()
.background(Color.white)
.cornerRadius(20)
.shadow(radius: 20)

It’s worth noting here that the frame(), padding() etc. modifiers affect the HStack. They’re tacked onto the HStack() { } initializer function. The modifiers look like they’re “outside” the HStack, but the syntax here is no different from the Text() and .font() we’ve used before.

How do these modifiers work?

  • The frame() modifier adjusts the maximum width of the view. That maxWidth is just one of its parameters; it’s got minWidth, width, etc., too.
  • The padding() modifier adds some space around the view, on its inside, i.e. “padding”. The sensible default of this function is 20 points.
  • The background() modifier sets the background color of the view to white.
  • The cornerRadius() modifier adds round borders to the view. The distance between the start of the round edge to the corner of the view is 20 points.
  • The shadow() modifier adds a drop shadow to the view. It’s got a smoothing radius of 20 points and a default black/gray color.

Modifiers in SwiftUI often – not always – return a new view. Because of this, the order of modifiers is important. If you switch the places of the padding() and background() modifiers, for example, the white background won’t extend to the edge of the view.

Why is that? It’s because the padding() function returns a View value on which background() is called. You want the background to extend behind the padding, which is why padding() needs to get called first. The order of modifiers is intuitive, you call modifiers from left-to-right or top-to-bottom.

Before we move on to the next section, quickly add another round border to the Image view – for good measure:

Image(“reinder”)
.cornerRadius(5)
Awesome!

A Closer Look at SwiftUI’s Syntax

Let’s circle back for a bit and discuss the composition of a view in SwiftUI. Here’s what we’ve made, sans modifiers:

struct CardView: View {
var body: some View {
HStack {
VStack {
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}
Image(“reinder”)
}
}
}

First things first. Every SwiftUI view, which conforms to the View protocol, has one property called body. This is a computed property that returns one value. We build our one subview inside that view body.

Thanks to the some keyword and opaque return types, the implementation of the view, i.e. the code that’s inside body, determines the concrete type of the body property. This way we don’t have to change the type of body when its implementation changes.

Now that we know that body is a computed property, what value does it return? The top-level view in CardView is a HStack. But… where’s the return keyword? Thanks to some syntactic sugar in Swift 5, that return keyword is omitted!

struct CardView: View {
var body: some View {
return HStack {

}
}
}

See? We’re just removing the return keyword to make the SwiftUI syntax more concise and readable. This obscures the fact that the body property returns one value – like any other Swift function – so that’s important to point out.

Remember when we tried to add 3 Text views to the CardView body? That won’t work!

struct CardView: View {
var body: some View { // won’t work!!
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}
}

That’s why we added the VStack, to show the Text views in a vertical stack. Like this:

VStack {
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}

But… why can’t a View have multiple subviews, but a VStack can? In the above code, the VStack clearly has 3 subviews! What’s going on?

At this point, it’s essential to know that the syntax for SwiftUI royally makes use of closures, trailing closure syntax, and generics. In reality, the above VStack amounts to the following explicit Swift code:

VStack(content: {
return TupleView<(Text, Text, Text)>((Text(“Reinder de Vries”), Text(“iOS Developer”), Text(“@reinder42”)))
})

Whoah, what’s that mess!? What you’re looking at here is a VStack initializer. It takes a parameter content, whose type is a closure that returns a TupleView. A TupleView is a SwiftUI view that’s created from a tuple of views, kinda like a container. It uses generics, so we’re specifying the concrete type of the values inside the tuple. Yes, those are double parentheses (()), one for the tuple, one for the initializer parameters…

Take a moment to appreciate that the above explicit notation of the VStack can change at any moment, when you change the CardView to something else. That’s the power behind the SwiftUI framework. Because of the generic nature of SwiftUI, a value’s type can change to something better, while remaining strongly typed.

It gets even more crazy than that, but we won’t go there. This much is clear: SwiftUI consists in part of syntax around generic types and closures. It’s syntactic sugar for composable types – views – which we can put together with as little code as possible.

Back to Syntactic Sugar World…

VStack {
Text(“Reinder de Vries”)
Text(“iOS Developer”)
Text(“@reinder42”)
}

Aah, much better! But now you know: Those 3 Text views actually comprise a tuple that’s provided as the return type of a closure that’s provided to the VStack initializer. It’s mind-boggling…

Note: The term syntactic sugar means that a programming language has syntax that obscures some implementation with nicer, prettier syntax. The implementation doesn’t change, but the code and syntax itself does – which makes all the difference. Code is there for the programmer, to the computer it’s bits and bytes anyway.

Further Reading

Awesome! We’ve taken a deep-dive into SwiftUI and how you can use views and modifiers to build UIs. Here’s what we’ve discussed:

  • How SwiftUI’s syntax and composition works
  • What views and modifiers are, and how they work
  • How to work with VStack, HStack, Text, Spacer and Image views
  • Using modifiers like frame(), font(), padding() and shadow()
  • Tips and tricks to work productively with SwiftUI in Xcode
  • Customizing SwiftUI views with code, as well as SwiftUI Inspector
  • Working with Live Preview in Xcode
  • How SwiftUI uses closures and generics to enable a concise syntax


Kasib Khan

Billing Advisor at Appy Pie

App Builder

Most Popular Posts