marinbenc🐧
Posted on February 11, 2020
SwiftUI is a unifying framework in more ways than one. Whether you're building an Apple Watch, Apple TV, iPhone, iPad or even Mac apps, SwiftUI will let you do that. Whether you're a designer, web developer or simply someone interested in building apps, SwiftUI is still for you. It might take a bit longer than for seasoned iOS veterans, but you'll get the hang of things!
Once you're on the other side of this course, you won't just be an iOS developer you'll be a SwiftUI developer. Capable of building for all Apple platforms!
This course will guide you through building a real-world chat application. You'll build each part of the app including logging the user in, displaying their contacts and chatting over the internet. By building a real app, you'll quickly learn practical SwiftUI skills that you can use in the real world.
In this first part, you'll make a welcome screen for the app. You'll learn about existing SwiftUI views, how to make your own views, how to lay them out and space them as well as how to style them to your liking.
On your way to victory, SwiftUI might try to sabotage you at some point with its opaque error messages or hard-to-find APIs. Remember, fortune favors the brave, so let's dive in with courage!
You can find the finished project of this part of the SwiftUI course here.
Making SwiftUI Views
We'll start our SwiftUI adventure by building out a welcome screen for our app. It will be a static screen with some information, an image and buttons to either log in or sign up to our chat service.
A SwiftUI app is a huge tree of View
s. View
is a SwiftUI protocol that represents any view that can be shown on the screen, whether it's a huge detail screen or a simple label.
Let's start by creating a new SwiftUI app. Open up Xcode. For this to work, you'll need Xcode 11 or later.
I am creating this course in collaboration with CometChat, a modern chat platform to help you add chat to you Swift app. During the next few weeks, we’ll be releasing installments of our free SwiftUI course here on dev.to! In the course, you’ll dive deep into SwiftUI by building a real-world production-scale chat app, learning SwiftUI in a practical way, on a scale larger than a simple example app. Follow me to get notified of future parts of this course! You can also follow @CometChat on Twitter see the course of CometChat’s blog.
On the Xcode welcome screen, click Create a new Xcode project. Select the Single View App template under iOS and hit Next. Name the app anything you'd like. I named my app "CometChat". Make sure SwiftUI is selected as the User Interface and click Next again. Select a good location to save the project and click Create.
Congrats! You just made your first SwiftUI app. This is the end of this course. Just kidding.
Next, you'll create a new SwiftUI view. Click File > New > File... and, as the template, select SwiftUI View and name it WelcomeView.
You'll get presented with an almost empty struct, except for one computed property called body
. This is the most important part of a view. body
returns the contents of your view. iOS will periodically call this computed property to re-draw what's on the screen.
You might have noticed body
's type is some View
. View
is a protocol, and the some
keyword tells Swift that body
can be any concrete type, as long as it conforms to View
.
Change the contents of the struct to the following:
struct WelcomeView: View {
var body: some View {
Text("Create an account")
}
}
On the right of your code, you'll see the Canvas. This is where you can preview the views you're making. If the preview updating is paused, click the Resume button in the top-right corner.
As you make changes to your code, the Canvas will update to show your changes. At least in theory. Often the canvas will be too quick to update and try to compile partially-written code or run into some other issue. You'll get used to clicking Try Again and Resume a lot. :)
Arranging SwiftUI views with stacks
Let's add another label to the view, below the one you just added. You might be tempted to simply add another Text
below the current one, but if you do that you'd get a compiler error. The error happens because body
is a single View
: You can't return two values. Instead, you need a view that would wrap around the two texts, like a SwiftUI birthday present.
SwiftUI has a couple of different views that group other views and the one you'll use the most is VStack
and HStack
. These are the SwiftUI equivalents to UIKit's UIStackView, stacking for vertical and horizontal stack views, respectively. If you're coming from web development, VStack
and HStack
are the Flexbox of SwiftUI.
These two views are your primary layout tool in SwiftUI. By arranging items into stacks, nesting stacks within stacks and controlling stack alignment and item spacing you can make almost any layout imaginable.
Let's change body
so that it returns a vertical stack of two text views:
var body: some View {
VStack {
Text("Create an account")
Text("Connect with people around the world")
}
}
You'll see the two labels centered in the middle of the preview screen.
If you look at the screen we're trying to make, though, the two texts need to be aligned to the left and sit on top of the view. You can align items in a stack by passing the alignment in the stack's initializer:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
Text("Connect with people around the world")
}
}
Now the texts are aligned to the left, but they're still not on the top of the screen.
Let's look at how we can fix that.
Vertically aligning VStack items with spacers
The stack's alignment
controls the alignment of the items along the opposite axis of the stack, so for VStack
, it controls the horizontal alignment. To align items along the major axis you can either manually space the items or use a special view called Spacer
.
A Spacer
, as its name suggests, is an empty view that stretches out to fill out as much space as it can. When you place it inside a VStack
, it will stretch horizontally. If you place it in an HStack
, it stretches vertically.
Add a spacer to the bottom of your stack to see what happens:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
Text("Connect with people around the world")
Spacer()
}
}
If VStack
is the birthday present, the spacers are the birthday party balloons, filling up the horizontal space as long as they can, pushing the two text views to the top of the screen.
To get the feel for how spacers behave, experiment by placing spacers at different positions inside the stack. Here are a couple of examples:
As you can see, by placing spacers at different positions you can align and space out your items anywhere in the stack. To fine-tune the spacing, you'll use padding — but you'll read more on that later in this part of the SwiftUI course.
For now, we'll take a break from laying out our view and venture into styling the text.
Styling SwiftUI views with View
methods
View
s have all kinds of methods to tweak their properties, whether its changing their size, colors, state, opacity or anything else that can be changed. Some of these methods are common to all views, like methods for changing the frame or padding. Others are specific to that view, such as the font of a Text
or the enabled state of a Button
.
According to the screenshot of the view we're building, the second text in our stack needs to have a big and bold font. To make this change, call the font
method on the text:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
Text("Connect with people around the world")
.font(Font.largeTitle.weight(.bold))
Spacer()
}
}
If you're used to UIKit, you're probably familiar with classes like UIFont
or UIColor
. SwiftUI has its equivalents, but drops the UI
prefix and makes them a struct.
There's also one big difference between the UIKit and SwiftUI versions: SwiftUI's fonts and colors are late binding. This means that a Font
is just a token or an identifier for some font. This token only gets resolved to a specific font during runtime, depending on the environment it's running in. For instance, Font.largeTitle
gets resolved to San Francisco on iOS, but could be Helvetica on the web.
SwiftUI modifier methods are non-mutating. Instead, they return a new View
with the changes applied. This has two consequences. First, you can chain modifier calls one after the other.
Image("welcome")
.resizable() // returns a new Image
.aspectRatio(contentMode: .fit) // returns a new View
.padding(.bottom, 35) // returns a new View
Secondly, because each of these methods creates a new view, the order of calling the methods can sometimes be important. A modifier called later can override what you set earlier, like in CSS or when subclassing a Swift class.
If you're the kind of developer who prefers a GUI over plain code, you can Control-Option-Click a SwiftUI view (either in the code or the preview) and open the SwiftUI Inspector.
This opens up a popup window that will let you tweak the settings of a view. As you make changes, SwiftUI will generate code for those changes and add them to your body
.
Changing the color of a SwiftUI Text
While we're beautifying our views, why not add some color. As you can guess by now, adding color is a simple matter of calling a method on the label:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
Text("Connect with people around the world")
.font(Font.largeTitle.weight(.bold))
.foregroundColor(.accentColor)
Spacer()
}
}
Just like Font
, Color
is also a late-binding token, which means that the actual color value of the color above will depend on the current environment, including what platform it's running on as well as whether it's running in light or dark mode.
Even SwiftUI Color
s with specific names like .blue
will result in a different color in light and dark mode. These are called adaptive colors, and you can make your own!
Adding your own adaptive colors
You'll add a new color for the text content of your app. Open Assets.xcassets in the main folder of your app in Xcode. Right-click on the list of assets and select New Color Set. At first, you'll see a white color with "Universal" below it. But, if you take a look at the Attributes Inspector, you'll see there's a bunch of different environment combinations you can set.
For instance, you can set one color for CarPlay in dark mode, a different color for Apple TVs in dark mode, and a third color for iPhones.
For now, select only Universal in the Devices list, and Any, Dark in the Appearances dropdown. This allows you to set a different color for light and dark mode on all devices.
Select the Any Appearance color and in the Attributes Inspector select 8-bit Hexadecimal for the Input Method and enter the hex value #2D313F
.
For Dark Appearance plain white will suffice. Select the Color in the sidebar and rename it to body. This name is important because that's how we'll access it in code.
Create a new plain Swift file called Colors.swift. Replace the contents of the file with the following:
import UIKit
import SwiftUI
extension Color {
static let body = Color("body")
static let cometChatBlue = Color(red: 27 / 255, green: 71 / 255, blue: 219 / 255)
static let shadow = Color(red: 27 / 255, green: 71 / 255, blue: 219 / 255, opacity: 0.3)
static let background = Color(red: 248 / 255, green: 249 / 255, blue: 251 / 255)
}
extension UIColor {
static let body = UIColor(red: 45 / 255, green: 49 / 255, blue: 63 / 255, alpha: 1)
}
This adds new static properties to Color
and UIColor
. We can use these colors throughout our app without having to copy and paste color names or specific values. Having everything in one place also means we can easily change these colors later.
Now that you have the color you can use it to make your text prettier. Head back to WelcomeView.swift and add a color to the first text:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
.foregroundColor(.body)
Text("Connect with people around the world")
.font(Font.largeTitle.weight(.bold))
.foregroundColor(.cometChatBlue)
Spacer()
}
}
Extending Color
with static properties is a great way to add reusability to our app. You can go a step further to make any changes to a view reusable. Let's do that next.
Styling SwiftUI views with view modifiers
So far you have a title and a body text styled as they look in the screenshot above. You'll use the same text treatment (font and color combination) throughout your whole app. Instead of copying and pasting all the method calls in every View
with texts, it would be better to have a way to mark titles and body text in a more reusable way.
SwiftUI lets you do that with View Modifiers. Structs and classes in your app can implement the ViewModifier
protocol which has one required method that receives and returns a view. The idea is that you receive a view, perform any necessary transformations and return it back to the caller.
You'll add two modifiers, one for the body text and one for titles. Create another plain Swift file and name it TextModifiers.swift. Add a new view modifier to the file:
import SwiftUI
struct TitleText: ViewModifier {
func body(content: Content) -> some View {
content
.font(Font.largeTitle.weight(.bold))
.foregroundColor(.cometChatBlue)
}
}
You implement the required method by receiving a view, applying a new font and foreground color and returning the changed view.
Similarly, add another struct below the one you just created for the body text:
struct BodyText: ViewModifier {
func body(content: Content) -> some View {
content
.foregroundColor(.body)
}
}
For the body text, you'll leave the font as it is and only change the color. Now, you can go back to WelcomeView.swift and replace the styling you applied with the modifiers by calling the modifier
method on the text views:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
.modifier(BodyText())
Text("Connect with people around the world")
.modifier(TitleText())
Spacer()
}
}
You can now use these modifiers in any future views. When you want to change the look of your apps content, you can change the modifier the apply the changes everywhere in your app.
Since view modifiers return a View
, you have the complete power of SwiftUI inside a modifier. This means you can even do complex changes like embed the view in a stack, scale it, adjust its spacing, add overlays and make whichever changes you can think of.
Spacing SwiftUI views with padding
I think that's enough styling for now. It's time to get back to laying out our screen with padding. SwiftUI doesn't have Auto Layout or constraints, but that doesn't mean you can't do everything you did in UIKit. It might be even easier. At least sometimes.
The method you use for this is called padding
, and it has two optional parameters:
func padding(
_ edges: Edge.Set = .all,
_ length: CGFloat? = nil)
-> some View
The first parameter is the set of edges to apply the padding to, while the second is the actual value of the padding. By default, the first parameter applies the padding to all edges. If you omit the length, it will be nil
which will apply the default system padding to the view.
Usually, you'd want to apply the default padding. The advantage of the default padding is that it's automatically adjusted to the device you're using. Smaller devices will have a smaller padding and vice-versa.
Change the body
of your view to add a padding to both of the texts:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
.modifier(BodyText())
.padding()
Text("Connect with people around the world")
.modifier(TitleText())
.padding([.bottom, .leading, .trailing])
Spacer()
}
}
To the body text, you add a default padding to every edge by calling padding()
. The title text doesn't need a padding on top, so you add the system padding only to the bottom, leading (left) and trailing (right) edges.
Adding padding to a SwiftUI view
You can combine the first and second parameter of padding
to achieve your desired effect. Here are some examples of different ways you can call padding
:
-
padding()
applies system padding to all edges. -
padding(.leading)
applies the system padding to the left edge. -
padding([.bottom, .leading, .trailing])
applies the system padding to three edges. -
padding(.top, 20)
adds a padding of 20 points to the top edge. -
padding([.bottom, .top], 0)
makes the top and bottom padding zero.
By combining stacks and padding you can intuitively make pixel-perfect layouts. At least, once you get used to it. :)
Nesting SwiftUI stacks
In UIKit, one of the most common view types are stack and table views. The same goes for SwiftUI: Your views will often be a big tree of nested stacks. In the screenshot of the screen you're making there's an image with a text below the two texts you just created.
Both the image and the text are centered, so add another VStack
inside the existing one, that you'll center horizontally:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
.modifier(BodyText())
.padding()
Text("Connect with people around the world")
.modifier(TitleText())
.padding([.bottom, .leading, .trailing])
VStack(alignment: .center) {
Text("""
This is a sample app.
Create an account or login to begin chatting.
""")
.modifier(BodyText())
.multilineTextAlignment(.center)
.padding([.leading, .trailing], 40)
}
Spacer()
}
}
You add another VStack
that will hold the image and the text. The text also has the body text modifier. You make sure the text is centered and add a padding of 40 points on the leading and trailing edges, so it doesn't touch the edges of the screen.
Displaying images in SwiftUI
Now that we added the text that's below the image, it's time to add the image. Image views in SwiftUI are called Image
, and they can be tricky to parse at first.
Before you add an image view, you first need to add the image itself. You can find the welcome image here.
Open Assets.xcassets and drag the image into the list of assets. Make sure the name is welcome.
Now, you can add a new Image
to the inner VStack
:
VStack(alignment: .center) {
Image("welcome")
.padding(.bottom, 35)
.padding([.leading, .trailing], 80)
Text("""
This is a sample app.
Create an account or login to begin chatting.
""")
.modifier(BodyText())
.multilineTextAlignment(.center)
.padding([.leading, .trailing], 40)
}
You give the Image
the name of the asset you created. Give it a padding of 35 points at the bottom (between the image and the text), as well as a padding of 80 on the sides.
The image looks good, but there's a small issue: Our whole UI is now broken.
It would be ideal if the image scaled according to the device size and how much space it has to grow. On an iPhone SE, the image should be smaller than on an 11 Pro Max.
We'll deal with this issue in a second, but let's first take a look at how we can spot issues like this in the future.
Previewing SwiftUI views on a different device
You can tweak the SwiftUI preview to see your views at different sizes. The preview screen isn't magic: It's loaded from a struct in the file that implements PreviewProvider
. The preview provider is similar to a View
. It has one computed property where you return what should be drawn inside the Canvas.
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
You can change this view in the same way you can change any other View
. There are also preview-specific methods that control the size and look of the previews.
Call previewDevice
inside the previews
property and display an iPhone SE:
struct WelcomeView_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
.previewDevice(PreviewDevice(rawValue: "iPhone SE"))
}
}
You can now see that the image doesn't scale according to screen size. Let's fix that.
Scaling a SwiftUI Image
To make an Image
view resizable you need to, you guessed it, call the resizable
method! Add the following two new lines to body
:
VStack(alignment: .center) {
Image("welcome")
.resizable()
.aspectRatio(contentMode: .fit)
.padding(.bottom, 35)
.padding([.leading, .trailing], 80)
Text("""
This is a sample app.
Create an account or login to begin chatting.
""")
.modifier(BodyText())
.multilineTextAlignment(.center)
.padding([.leading, .trailing], 40)
}
By default the image will just scale to fill its available space, which you probably don't want. Usually, you want to maintain the image's aspect ratio so that it doesn't look stretched out.
To do that in SwiftUI, you can use the aspectRatio
method which receives a parameter telling it how to scale the image. .fit
scales the image so that it always fits inside its space, leading to letter-boxing. On the other hand, .fill
scales it so that it fills the whole space, but you won't see the whole image.
In the above code, you first call resizable
on the image to tell SwiftUI that it can stretch out the image if needed. Then, you call aspectRatio
with .fit
to make sure the image always fits within the screen, 80 points from each edge.
If you change the device back to iPhone 11
in the preview you'll notice the image now scales to fit its available size, no Auto Layout needed.
We're almost done with our welcome screen! The final step is to add the two buttons on the bottom.
Creating buttons in SwiftUI
To build out a screen like this in UIKit, you'd use a view controller, and house all of your view related things in there. Usually, this is not how you'll build SwiftUI views.
SwiftUI does away with the distinction between view and view controller and it's built so that you compose tons of tiny views to build your screen.
That's what you've been doing: Text
, VStack
and Image
are all small views that do one thing well, but by combining them you've created an already good-looking screen.
Now it's time we make your own small SwiftUI views that you can use throughout the app. These views will be the two buttons you see on the bottom of the screen you're building.
Create a new SwiftUI View
file called ButtonViews, and change the struct to the following:
import SwiftUI
struct PrimaryButton: View {
let title: String
var body: some View {
Text(title.uppercased())
}
}
You need to build a big blue button with rounded corners. You'll start with a good old text view.
A button can hold any other view, no matter how complex. With that in mind, our button is looking a little plain.
At this point, you'll get an error because the automatically generated preview references the wrong view name. Change the preview to show your button:
struct ButtonViews_Previews: PreviewProvider {
static var previews: some View {
PrimaryButton(title: "Primary")
}
}
We'll make it look better, but before we go on, let's do some preview setup to make our lives easier.
Changing the size of a SwiftUI preview
Earlier in this section of the course, you learned how to preview your SwiftUI views on different devices. Most of your views will be reusable UI components, so it doesn't make sense to show a whole iPhone with a tiny view in the middle of a blank screen.
Instead, you can preview them in a small window. Change the preview struct to the following:
struct ButtonViews_Previews: PreviewProvider {
static var previews: some View {
PrimaryButton(title: "Primary")
.previewLayout(.fixed(width: 300, height: 100))
}
}
Now the preview shows a small rectangle that houses our button.
Nice and neat! Let's get back to our buttons.
Styling SwiftUI buttons
Let's deal with that plain-looking button! Start by adding a background and a shadow to the button:
var body: some View {
Text(title.uppercased())
.background(Color.accentColor)
.cornerRadius(5)
.shadow(color: .shadow, radius: 15, x: 0, y: 5)
}
Okay, you now have a rounded blue blob, which is a start.
Next, change the color and add some padding around the text:
var body: some View {
Text(title.uppercased())
.fontWeight(.bold)
.foregroundColor(.white)
.padding()
.frame(maxWidth: .infinity)
.background(Color.accentColor)
.cornerRadius(5)
.shadow(color: .shadow, radius: 15, x: 0, y: 5)
}
There's one new method here: frame
. While padding determines how much space there is around the content, the frame determines how big the content is.
You can either specify a constant size or you can make suggestions to SwiftUI about how it should size the view. Here, you set the maximum width to infinite, making the view stretch to fit the size of its parent.
Our button is looking perfect! Let's add another, secondary button.
Creating the secondary button
To create the secondary button, you'll use a complex programming technique used by generations of programmers: copying and pasting. Because SwiftUI views are short pieces of code that are all in a single struct, you can easily copy and paste a view, modify it slightly and get a whole new look.
For the secondary button, copy over the primary button and change the colors a bit to match the screenshot:
struct SecondaryButton: View {
let title: String
var body: some View {
Text(title.uppercased())
.fontWeight(.bold)
.foregroundColor(.accentColor)
.padding()
.frame(maxWidth: .infinity)
.background(Color.white)
.cornerRadius(5)
.shadow(color: .shadow, radius: 15, x: 0, y: 5)
}
}
It looks great... I think. To see the button you need to change your preview first.
Showing multiple SwiftUI previews
You might be tempted to replace Primary
with Secondary
in the preview and call it a day. But, why show just one view, when you can show both of them at once?
If you wrap the views inside a Group
, the Canvas will show each grouped view as a separate preview.
static var previews: some View {
Group {
PrimaryButton(title: "Primary")
.previewLayout(.fixed(width: 300, height: 100))
SecondaryButton(title: "Secondary")
.previewLayout(.fixed(width: 300, height: 100))
}
}
Now you can see both of the buttons at once.
This is especially useful when you have views that can show multiple states. For instance, we can show a preview for buttons that have long titles:
static var previews: some View {
Group {
PrimaryButton(title: "Primary")
.previewLayout(.fixed(width: 300, height: 100))
PrimaryButton(title: "This button has a really long title")
.previewLayout(.fixed(width: 300, height: 100))
SecondaryButton(title: "Secondary")
.previewLayout(.fixed(width: 300, height: 100))
}
}
This makes it easy to see how your changes affect different states of your views, making sure you spot a lot of bugs without having to build and click around your app.
Using custom SwitUI views
With our buttons created, let's add them to the welcome screen. Back in WelcomeView.swift, add a new VStack
with the two buttons, the final addition to the welcome screen:
var body: some View {
VStack(alignment: .leading) {
Text("Create an account")
.modifier(BodyText())
.padding()
Text("Connect with people around the world")
.modifier(TitleText())
.padding([.bottom, .leading, .trailing])
VStack(alignment: .center) { ... }
// New code:
VStack(spacing: 30) {
Button(action: { }) {
PrimaryButton(title: "Log In")
}
Button(action: { }) {
SecondaryButton(title: "Sign Up")
}
}
.padding([.leading, .bottom, .trailing])
.padding(.top, 40)
// ---
Spacer()
}
}
You can use your View
s just like any other SwiftUI view. After all, they're nothing more than a Swift struct conforming to a protocol. In the above code, you placed your views inside a VStack
, and each inside a Button
.
As opposed to UIKit's UIButton
, Button
is a completely blank view. All it does is make its contents touchable and manages button state changes. How the button looks is completely up to you. In the code above, you used PrimaryButton
and SecondaryButton
to display a button label. Instead of that, you could have used Text
, Image
or any other SwiftUI view.
Currently, the buttons don't do anything. But, if you run the project and tap on the buttons, you'll notice they light up as you tap them. That animation is coming from Button
.
Earlier in this part of the course you used padding to space out views within a stack. If you want the space between each of the items to be the same, you can pass the spacing
parameter to a stack's initializer. The stack will then take care of the spacing for you. You can even use padding a spacing in tandem for those pixel-perfect designers among you!
And with that, your welcome screen is done! This now looks exactly like the original screenshot, giving the user some information about the app and a way to log in and register. Right now, these buttons don't do anything. In the next part of this SwiftUI course, you'll navigate to a login and registration screen!
Conclusion
In the span of one part of this course, you went from having no app and no SwiftUI knowledge to building out your first full SwiftUI screen! With this knowledge and some googling, you now have the skills to build almost any static SwiftUI screen.
You know about views, how to create them, lay them out and space them around. You also learned about adding and customizing text, creating buttons and displaying images.
SwiftUI shows its power here: By learning a small number of tools you can build out any UI you can imagine. That's elegant, flexible design.
Your layout knowledge doesn't need to end here, though. For the A-grade students among you, check out the WWDC session called "Building Custom Views with SwiftUI" which goes more in-depth into how the layout system works and some other tricks you can use.
There's still much to learn! You now know how to make static UIs, but in the next part, you'll learn how to navigate to a login screen and manage the state for your UIs. Keep reading to build out the login screen of your app!
Posted on February 11, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.