5.
Intro to Controls: Text & Image
Written by Antonio Bello
From what you’ve seen so far, you’ve already figured out what level of awesomeness SwiftUI brings to UI development. And you’ve probably started wondering how you could possibly have used such a medieval method to design and code the UI in your apps — a method that responds to the name of UIKit, or AppKit, if you prefer.
In the previous chapters, you’ve only scratched the surface of SwiftUI and learned how to create some basic UI. Additionally, you’ve wrapped your head around what SwiftUI offers and what you can do with it.
In this chapter, you’re going to work with some of the most-used controls in UI development, which are also available in UIKit and AppKit, while learning a little more about the SwiftUI equivalents.
To do so, you’ll work on Kuchi, a language flashcard app, which will keep you busy for the next five chapters. Enjoy!
Getting started
First, open the starter project for this chapter, and you’ll see that it’s quite empty. There’s almost no user interface; only some resources and support files. If you build and run, all you’ll get is a blank view.
In the Project Navigator, locate the Shared group, then right click on it, choose New Group, and rename as Welcome
Next right-click on it, and choose New File.
In the popup that comes next, choose SwiftUI View, then click Next.
Then type WelcomeView in the Save As field, ensure that both iOS and macOS targets are selected, and click on Create. You now have a blank new view to start with.
Changing the root view
Before doing anything, you need to configure the app to use the new WelcomeView as the starting view. Open KuchiApp, and locate the body property, which contains an EmptyView inside a WindowGroup.
var body: some Scene {
WindowGroup {
EmptyView()
}
}
This code determines the view that’s created and displayed when the app is launched. The view currently created is EmptyView, which is… well, an empty view: the simplest possible view you could possibly use. Replace it with an instance of the new view you’ve just created, WelcomeView:
WindowGroup {
WelcomeView()
}
Now, if you compile and run, when the app starts, WelcomeView will be your first view:
While you’re on it, also replace EmptyView in the preview, which looks like:
struct KuchiApp_Previews: PreviewProvider {
static var previews: some View {
EmptyView()
}
}
And, after the replacement, must look like:
struct KuchiApp_Previews: PreviewProvider {
static var previews: some View {
WelcomeView()
}
}
WelcomeView!
Now, take a look at the newly created view. Open WelcomeView, and you will notice there isn’t much in it:
- The
WelcomeViewstruct, containing thebodyproperty, and aTextcomponent. - A preview provider named
WelcomeView_Previews.
But that’s all you need to get started. body is the only thing a view requires — well, besides implementing a great and stylish UI, but that’s your job!
In Xcode, make sure that you have the canvas visible in the assistant panel, and click the Resume button if necessary, to activate or reactivate the preview. You should see a welcome message like this:
Text
Input requires context. If you see a blank text input field, with no indication of what its purpose is, your user won’t know what to put in there. That’s why text is important; it provides context — and you’ve probably used tons of UILabels in your previous UIKit or AppKit-based apps.
As you’ve already seen, the component to display text is called, simply, Text. In its simplest and most commonly used initializer, Text takes a single parameter: the text to display. Change the string to “Welcome to Kuchi”:
Text("Welcome to Kuchi")
Xcode will automatically update the text shown in the preview. Nice! Simple stuff so far, but every long journey always starts with a single step.
Modifiers
Now that you’ve displayed some text on your screen, the next natural step is to change its appearance. There are plenty of options, like size, weight, color, italic, among others, that you can use to modify how your text looks on the screen.
Note: In the previous chapters, you’ve already learned how to use a modifier to change the look or behavior of a view. A modifier is a view instance method that creates a copy of the view, does something to the view copy (such as changing the font size or the color), and returns the modified view.
To change the look of a Text instance, you use modifiers. But beyond that, more generally, any view can be altered using modifiers.
If you want to make the text larger, say, 60 points, add the following font modifier:
Text("Welcome to Kuchi")
.font(.system(size: 60))
Then bold the text by adding the next line:
Text("Welcome to Kuchi")
.font(.system(size: 60))
.bold()
Then you can make it a nice red color:
Text("Welcome to Kuchi")
.font(.system(size: 60))
.bold()
.foregroundColor(.red)
Next, you can center-align the text:
Text("Welcome to Kuchi")
.font(.system(size: 60))
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center)
And, finally, you can force it to be rendered in one line:
Text("Welcome to Kuchi")
.font(.system(size: 60))
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center)
.lineLimit(1)
Which doesn’t look nice… but it’s good to know that you can limit the number of lines, considering that by default lineLimit is nil, meaning that the text will take as many lines as needed.
So it would definitely look better if you limit the number of lines to two:
Text("Welcome to Kuchi")
.font(.system(size: 60))
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center)
.lineLimit(2)
Although it’s safe to assume that default values for this UI component won’t change, they might change in the future. For example, in SwiftUI 1.0 the default value for lineLimit was 1, but it’s been changed to nil in 2.0. Likewise, the text alignment was .center in SwiftUI 1.0, but it became .leading in 2.0.
That is to say it’s good practice to not rely too much on default values, because they can change in future versions.
So far you’ve exclusively used code to add and configure modifiers, but SwiftUI, in tandem with Xcode, offers two alternatives for the lazy, er, I mean efficient coders out there:
- A popup canvas inspector, which appears when you Command-click on a view component onto the canvas:
- The attributes inspector, which appears by pressing Option-Command-4, and displays the modifiers for the view currently selected in the canvas:
Text is such a simple component, but it has so many modifiers. And that’s just the beginning! There are two categories of modifiers that SwiftUI offers:
- Modifiers bundled with the
Viewprotocol, available to any view. - Modifiers specific to a type, available only to instances of that type.
View has lots of premade and ready-to-use modifiers that are implemented in protocol extensions. For a full list, you can browse the documentation; in Xcode, Option-click View in the source editor, and then click Open in Developer Documentation.
Browsing the documentation is always helpful when learning, but sometimes you need a faster way to search for a modifier. Maybe you don’t remember the modifier’s name, or maybe you are simply wondering if such a modifier exists.
Again, Xcode and SwiftUI can help with that! As you might remember from Chapter 3: Diving Deeper Into SwiftUI, Xcode now has a Modifiers Library, similar to the Object Library available in older versions of Xcode.
To access the library, click the leftmost + button, located at the top-right corner of your Xcode window. The library allows you to browse and search by name, and, most importantly, groups all modifiers by category, so chances are that you’ll quickly find what you’re looking for, if it actually exists.
Note that the library also contains the Views Library, which you can use to browse and select views, and drag them onto the canvas, for two-way user interface development.
Are modifiers efficient?
Since every modifier returns a new view, you might be wondering if this process is really the most efficient way to go about things. SwiftUI embeds a view into a new view every time you invoke a modifier. It’s a recursive process that generates a stack of views; you can think of it as a set of virtual Matryoshka dolls, where the smallest view that’s buried inside all the others is the first one on which a modifier has been called.
Intuitively, this looks like a waste of resources. The truth is that SwiftUI flattens this stack into an efficient data structure that is used for the actual rendering of the view.
You should feel free to use as many modifiers as you need, without reserve and without fear of impacting the efficiency of your view.
Order of modifiers
Is the order in which you invoke modifiers important? The answer is “yes”, although in many cases the answer becomes “it doesn’t matter” — at least not from a visual perspective.
For example, if you apply a bold modifier, and then make it red:
Text("Welcome to Kuchi")
.bold()
.foregroundColor(.red)
..or first make it red, and then bold:
Text("Welcome to Kuchi")
.foregroundColor(.red)
.bold()
…you won’t notice any difference.
However, if you apply a background color and then apply padding, you will get a different result. .padding is a modifier that adds spacing between the view the modifier is applied to and the view’s parent. Without parameters, SwiftUI adds a default padding in all four directions, but you can configure that padding yourself.
Consider the following configuration below:
Text("Welcome to Kuchi")
.background(Color.red)
.padding()
You add a red background color to the text, and then apply padding. But if you invert that order:
Text("Welcome to Kuchi")
.padding()
.background(Color.red)
You apply the padding first, resulting in a larger view, and then apply the red background. You’ll immediately notice that the result is different:
This is because the view where you apply the background color is different in each case. Another way to look at it is that the view to which you apply the padding is different.
This is clearly visible if you set different background colors before and after applying the padding:
Text("Welcome to Kuchi")
.background(Color.yellow)
.padding()
.background(Color.red)
The padding adds some space between the text and the edges of the view. When you apply the background color before the padding, that modification is applied to the view that contains the text, which is a view large enough to contain just the displayed text and nothing more. The padding modifier adds a new view, to which the second background color is applied to it.
Image
An image is worth a thousand words. That may be a cliché, but it’s absolutely true when it comes to your UI. This section shows you how to add an image to your UI.
First, remove the welcome Text from body and replace it with an Image component as shown below:
var body: some View {
Image(systemName: "table")
}
This is what you’ll see on screen:
Changing the image size
When you create an image without providing any modifiers, SwiftUI will render the image at its native resolution and maintain the image’s aspect ratio. The image you’re using here is taken from SF Symbols, a set of icons that Apple introduced in the 2019 iterations of iOS, watchOS and tvOS and that we have already used in previous chapters. For more information, check out the links at the end.
If you want to resize an image, you have to apply the resizable modifier, which takes two parameters: an inset and a resizing mode. The resizing mode can be either .tile or .stretch.
If you don’t provide any parameters, SwiftUI assumes no inset for all four directions (top, bottom, leading and trailing) and .stretch resizing mode.
Note: If you don’t apply the
resizablemodifier, the image will keep its native size. When you apply a modifier that either directly or indirectly changes the image’s size, that change is applied to the actual view the modifier is applied to, but not to the image itself, which will retain its original size.
So if images are worth a thousand words, then code examples must be worth a thousand images! To embed an image in a square frame, 30 points wide and high, you simply add the frame modifier to the image:
var body: some View {
Image(systemName: "table")
.frame(width: 30, height: 30)
}
The preview won’t show any difference; you’ll still see the image at its original size. However, if you click the image to select it, Xcode will show the selection highlight as a blue border:
The outermost view has the correct size, but, as you may have expected, the image didn’t scale to match.
Now, prepend frame with the resizable modifier:
var body: some View {
Image(systemName: "table")
.resizable()
.frame(width: 30, height: 30)
}
The output should be a lot closer to what you expected:
Note: You’ve given the image an absolute size, measured in points. However, for accessibility reasons, and to help your app adapt to different resolutions, orientations, devices and platforms, it’s always a good idea to let SwiftUI decide how to scale images, and more generally, most of your UI content. You’ll cover that briefly in this chapter, but you’ll go into scaling more in-depth in the next chapter.
If you want to transform and manipulate that image to make it look like a bordered and circular red-colored grid with a light gray background, add the following code after .frame:
// 1
.cornerRadius(30 / 2)
// 2
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
// 3
.background(Color(white: 0.9))
// 4
.clipShape(Circle())
// 5
.foregroundColor(.red)
Here’s what you’re doing:
- You set the corner radius to half the size of the image.
- Next, you add a thin gray border.
- You then add a light gray background color.
- Next, you clip the resulting image using a circle shape, which removes the excess colored background.
- Finally, you set the foreground color to red.
Here’s how the sequence of modifiers affects the resulting image at each step:
It turns out one of the modifiers in the previous code is redundant. If you remove that modifier, the resulting image is the same. Can you tell which modifier is redundant?
It might not be obvious at first glance, but the corner radius, which makes the image circular, actually clips the image. But isn’t that what the shape clipping at the 4th line is doing? Try it out! Delete or comment out the corner radius modifier, and you’ll see that the resulting image doesn’t change.
You can safely remove that line of code - But it’s good to know how to apply a corner radius to a view.
Last thought for this section: have you considered how easy it was to manipulate and transform an image with just a few lines of code? How many lines of code would you have written in UIKit or AppKit to achieve the same result? Quite a lot more, I believe.
Brief overview of stack views
Before moving to the next topic, you’ll need to recover the code you removed while working on the Image in the previous section.
To add the Text view again, alter the implementation of body so it looks as follows — here the Text font size has been reduced to from 60 to 30, otherwise it would look too big compared to the image:
Image(systemName: "table")
.resizable()
.frame(width: 30, height: 30)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.background(Color(white: 0.9))
.clipShape(Circle())
.foregroundColor(.red)
Text("Welcome to Kuchi")
.font(.system(size: 30))
.bold()
.foregroundColor(.red)
.lineLimit(2)
.multilineTextAlignment(.center)
Note that this is not the correct way to add multiple subviews to a view. With a few exceptions, the View’s body property expects one and only one subview.
In SwiftUI 1.0, the code above would have caused a compilation error, now it compiles and even works: all subviews will be stacked vertically. However if you preview it in Xcode, it will show one preview per subview.
If you want to embed more than one subview in a view, you have to rely on a container view. The simplest and most commonly used container views is the stack, the SwiftUI counterpart of UIKit’s UIStackView.
Stacks come in 3 different flavors: horizontal, vertical and, for the lack of a better term, on top of one another. For now we’ll use the horizontal version, which is the SwiftUI counterpart of UIKit’s UIStackView in horizontal layout mode. Embed the two views into an HStack:
HStack {
Image(systemName: "table")
...
Text("Welcome to Kuchi")
...
}
Note: You’ll learn about
HStackin Chapter 7: “Introducing Stacks & Containers”. All you need to know right now is thatHStackis a container view, which allows you to group multiple views in a horizontal layout.
This is how the view looks like in the Xcode preview:
More on Image
Two sections ago, you played with the Image view, creating an icon at the end of the process. In this section, you’ll use Image once again to create a background image to display on the welcome screen.
To do that, you need to know about another container view, ZStack, which stacks views one on top of the other, like sheets of papers in a stack — that’s why it’s been described with the on top of one another term.
This is different from HStack (and VStack, which you’ll meet later in this chapter) which arranges views next to one another instead.
Since you need to add a background image, ZStack seems to fit the purpose. Embed the HStack of the previous section inside a ZStack:
ZStack {
HStack {
...
}
}
Nothing changes in the canvas preview. Now, add this Image view before HStack, still inside of the ZStack:
Image("welcome-background", bundle: nil)
The image looks okay, but it has too much presence and color.
View and Image have a comprehensive list of modifiers that let you manipulate the appearance of an image. These include opacity, blur, contrast, brightness, hue, clipping, interpolation, and aliasing. Many of these modifiers are defined in the View protocol, so they’re not limited to just images; you could, theoretically, use them on any view.
Use this image as a reference to see what each modifier does. I encourage you to add modifiers one at a time in Xcode, to see the live result build up in the canvas preview:
The final code for the image should look as follows:
// 1
Image("welcome-background", bundle: nil)
// 2
.resizable()
// 3
.scaledToFit()
// 4
.aspectRatio(1 / 1, contentMode: .fill)
// 5
.edgesIgnoringSafeArea(.all)
// 6
.saturation(0.5)
// 7
.blur(radius: 5)
// 8
.opacity(0.08)
Going over this code, here is what you just did:
-
This is the
Imageyou’ve just added. -
.resizable: Make it resizeable. By default, SwiftUI tries to use all of the space at its disposal, without worrying about the aspect ratio. -
.scaledToFit: Maximize the image so that it’s fully visible within the parent, with respect to the original ratio. -
.aspectRatio: Set the aspect ratio, which is1:1by default. SettingcontentModeto.fillmakes the image fill the entire parent view, so a portion of the image will extend beyond the view’s boundaries. -
.edgesIgnoringSafeArea: Ignore the safe area insets, extending the view outside the safe area, so that it occupies the entire parent space.Here, you’re ignoring all edges, but it can also be configured on a per-edge basis. To do that, you pass an array of the edges to ignore:
.top,.bottom,.leading,.trailing, but also.verticaland.horizontal, which combine the two vertical and the two horizontal edges respectively. -
.saturation: Reduce the color saturation so that the image appears less vibrant. -
.blur: Add some blur. Who doesn’t love blur? -
.opacity: Make the image more transparent, which has the side effect of dimming the image to make it a little less prominent.
Once again, there’s a redundant modifier in that view. Can you figure out which one it was?
Yes, it’s the third line: the .scaledToFit modifier. You already made the image fit the parent with .resizable, and then .aspectRatio makes the image fill the parent instead. Comment the .scaledToFit modifier, and you’ll see that the final result doesn’t change.
Can you guess what happens if you switch scaledToFit and aspectRatio? Would you expect the final result to change?
You’ve probably figured it out already: scaledToFit overrides the fill mode set in the previous line — so now that becomes the redundant modifier. However, if you change the aspect ratio, to something like 2:
.aspectRatio(2 / 1, contentMode: .fill)
The result is quite different in this case, because you’re making the width twice as wide, while keeping the height unaltered:
That said, you can revert that aspect radio change, and safely delete the redundant .scaledToFit. The code for the background would then look like the following:
Image("welcome-background", bundle: nil)
.resizable()
.aspectRatio(1 / 1, contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.saturation(0.5)
.blur(radius: 5)
.opacity(0.08)
Splitting Text
Now that the background image is in good shape, you need to rework the welcome text to make it look nicer. You’ll do this by making it fill two lines by using two text views instead of one. Since the text should be split vertically, all you have to do is add a VStack around the welcome text, like so:
VStack {
Text("Welcome to Kuchi")
.font(.system(size: 30))
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center)
.lineLimit(2)
}
Next, you can split the text into two separate views:
VStack {
Text("Welcome to")
.font(.system(size: 30))
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center)
.lineLimit(2)
Text("Kuchi")
.font(.system(size: 30))
.bold()
.foregroundColor(.red)
.multilineTextAlignment(.center)
.lineLimit(2)
}
You may notice that the last three modifiers in each Text are the same. Since they are modifiers implemented in View, you can refactor the code by applying them to the parent stack view, instead of to each individual view:
VStack {
Text("Welcome to")
.font(.system(size: 30))
.bold()
Text("Kuchi")
.font(.system(size: 30))
.bold()
}
.foregroundColor(.red)
.multilineTextAlignment(.leading)
.lineLimit(2)
This is a very powerful feature: when you have a container view, and you want one or more modifiers to be applied to all subviews, simply apply those modifiers to the container.
Note: You might be wondering why you didn’t do the same thing for the first two modifiers of each contained view. Look at the documentation for
.fontand.bold, and you’ll see that these are modifiers on theTexttype. Therefore they aren’t available onViewandVStack.
To make the text appear nicer in respect to the image at its left, it’s better to make the two text views left aligned instead of centered. Because of the refactoring you’ve just done, you need to change that in one place only, instead of two:
.multilineTextAlignment(.leading)
But you may notice that it doesn’t work. That’s because you’ve split the text to two different Text, and each one is sized accordingly to its content, so changing the text alignment won’t have any visual effect.
In order to align the two Text views to the left, you have to change the alignment of the views contained in the VStack, which, by default, are centered - unfortunately there’s no modifier to change that, the only way is to specify the alignment in the initializer.
Remove the .multilineTextAlignment(.leading) modifier, and pass the alignment parameter to VStack as follows:
VStack(alignment: .leading) {
The line limit is also no longer needed. You could just remove it, but that would make the text free to span over multiple lines - unlikely to happen in this case, but just in case you can just ask each Text view to stay in one line only, by changing 2 to 1:
.lineLimit(1)
The VStack code should now look like:
VStack(alignment: .leading) {
Text("Welcome to")
.font(.system(size: 30))
.bold()
Text("Kuchi")
.font(.system(size: 30))
.bold()
}
.foregroundColor(.red)
.lineLimit(1)
Wouldn’t it be nice if the two lines of text had different font sizes? To achieve this, use the .headline style on the welcome text, replacing .font(.system(size: 30)) with the following:
.font(.headline)
For the Kuchi text, use a .largeTitle instead, replacing .font(.system(size: 30)) with the following:
.font(.largeTitle)
Finally, you’ll style the container slightly to make it a little less cramped. You’ll need some padding between the image and the text; you can use the .padding modifier and pass .horizontal, which adds padding horizontally on both sides. You could alternately pass other edges, such as top or leading, either standalone or as an array of edges. Also, you can specify an optional length for the padding. If you don’t specify this, SwiftUI will apply a default.
The code for the entire text stack should look like this:
VStack(alignment: .leading) {
Text("Welcome to")
.font(.headline)
.bold()
Text("Kuchi")
.font(.largeTitle)
.bold()
}
.foregroundColor(.red)
.lineLimit(1)
.padding(.horizontal)
The resulting view should appear as follows:
Markdown
New to SwiftUI 3.0, Text now supports a subset of markdown, which is a markup language for creating formatted text. If you don’t know what it is, check out the links at the end of this chapter.
Usage of markdown is possible because of the additions made to the new AttributedString type introduced in iOS 15 and macOS 12, which is the Swift “native” counterpart of the NSAttributedString type that you’ve probably used in the past. To make it clear, AttributedString is to NSAttributedString as String is to NSString.
The two Text components you’ve used above use the .bold() modifier to make the text bold. You can also get rid of it and use markdown to achieve the same result:
Text("**Welcome to**")
.font(.headline)
Text("**Kuchi**")
.font(.largeTitle)
Feel free to experiment with it and try other formatters, such as italic (*Kuchi* or _Kuchi_) and strikethrough (~~Kuchi~~). But keep in mind that only the following ones are currently supported:
- Bold
- Italic
- Strikethrough
- Inline code
- Link
Although in this simple example no method has a distinctive advantage over the other, the real power of markdown becomes perceivable when you need to apply formatting to substrings of a text.
For instance, if you want to print a text like “I am an awesome SwiftUI Software Engineer”, using “native” SwiftUI you’d have to use three different Text components (one for the unformatted text, one for the italic and one for the bold), whereas using markdown you’d use one Text, with its text set to I am an _awesome_ **SwiftUI Software Engineer**.
Accessibility with fonts
Initially, all of your views that display text used a font(.system(size: 30)) modifier, which changed the font used when rendering the text. Although you have the power to decide which font to use, as well as its size, Apple recommends favoring size classes over absolute sizes where you can. This is why, in the previous section, you used styles such as .headline and .largeTitle in place of .system(size: 30)
All sizes are defined in Font as pseudo-enum cases: They’re actually static properties. UIKit and AppKit have corresponding class sizes, so you probably already know a little bit about title, headline, body, or other properties like that.
Using size classes gives the user the freedom to increase or decrease all fonts used in your app relative to a reference size: if the reference size is increased, all fonts become larger in proportion, and if decreased, then the fonts become smaller. This is a huge help to people with eyesight issues or visual impairments.
That was a long journey! The concepts here are pretty simple but necessary to get you started in your SwiftUI development.
Before moving on, undo the changes to the Text components so they use .bold() instead of markdown - This was a brief introduction markdown, but you won’t use it in the Kuchi app.
Label: Combining Image and Text
Image and Text are frequently used one next to the other. Combining them is pretty easy — you just need to embed them into an HStack. However, to simplify your work, Apple has given you a component specifically for that purpose: Label.
Given the work you’ve done so far, resulting in a view with text and image, it’s time to test this new component.
Label has a few initializers, taking a raw string, and either a resource identifier or a system image identifier. For example, to display welcome text along with a waving hand icon, you’d write code like this:
Label("Welcome", systemImage: "hand.wave")
This displays a label as follows:
It also allows you to provide your custom view for the text and image. Since you’ve already put quite a lot of effort to customize your text and image, it’s the most appropriate choice to follow.
This initializer takes two parameters: a title and an icon, and it looks like this:
init(title: () -> Title, icon: () -> Icon)
To see it in action, you need to refactor this code:
HStack {
Image(systemName: "table")
.resizable()
.frame(width: 30, height: 30)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.background(Color(white: 0.9))
.clipShape(Circle())
.foregroundColor(.red)
VStack(alignment: .leading) {
Text("Welcome to")
.font(.headline)
.bold()
Text("Kuchi")
.font(.largeTitle)
.bold()
}
.foregroundColor(.red)
.lineLimit(1)
.padding(.horizontal)
}
So the Image goes to the Label’s icon parameter, and the VStack (along with its modifiers) to the title parameter.
Change the above code (HStack included) to:
Label {
// 1
VStack(alignment: .leading) {
Text("Welcome to")
.font(.headline)
.bold()
Text("Kuchi")
.font(.largeTitle)
.bold()
}
.foregroundColor(.red)
.lineLimit(2)
.multilineTextAlignment(.leading)
.padding(.horizontal)
// 2
} icon: {
// 3
Image(systemName: "table")
.resizable()
.frame(width: 30, height: 30)
.overlay(Circle().stroke(Color.gray, lineWidth: 1))
.background(Color(white: 0.9))
.clipShape(Circle())
.foregroundColor(.red)
}
Here’s what’s happening above:
- This is the text component, consisting of two
Texts embedded in a vertical stack. - Note how the new Swift 5.3’s multiple closures syntax is used here.
- This is the image component.
After applying this change, you notice, however, that it doesn’t look very good: The icon and the text are not vertically aligned.
To fix this, you need to override the way the label arranges its components. You can apply a style to a Label; the problem is that none of the available seems to fit with your needs:
-
DefaultLabelStyle: This is the default value, which corresponds to specifying no style at all. It displays both the title and the icon. -
IconOnlyLabelStyle: This displays the icon only, ignoring the title. -
TitleOnlyLabelStyle: This displays the title only, hiding the icon.
The good news is that if none fits, you can build your own. To create a custom style, you need to create a struct that adopts the LabelStyle protocol, which has one requirement only:
func makeBody(configuration: Self.Configuration) -> Self.Body
Add a new file to the Welcome group using the Swift File template, and name it HorizontallyAlignedLabelStyle.swift. Be sure to select both iOS and macOS targets.
Next, create an empty skeleton for the style:
import SwiftUI
struct HorizontallyAlignedLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
return EmptyView()
}
}
The configuration parameter contains both the text and the icon parameters passed to the Label initializer — all you have to do is embed them into an HStack:
func makeBody(configuration: Configuration) -> some View {
HStack {
configuration.icon
configuration.title
}
}
And that’s all! You’ve created custom style. To apply it, you need to add, you guessed it, a modifier to the Label that you added earlier in WelcomeView.swift.
In WelcomeView, at the bottom of Label add the following modifer:
.labelStyle(HorizontallyAlignedLabelStyle())
As you can see, the modifier is named labelStyle(), and it takes an instance of a label style, which, as mentioned earlier, is a type that conforms to the LabelStyle protocol.
This is how it looks like.
And yes, you don’t, and shouldn’t, see any difference to what you got in the version without the Label.
Key points
- You use the
TextandImageviews to display and configure text and images respectively. - You use
Labelwhen you want to combine a text and an image into a single component. - You use modifiers to change the appearance of your views. Modifiers can be quite powerful when used in combination, but remember to be aware of the order of the modifiers, because in some cases it does matter.
- Container views, such as
VStack,HStackandZStacklet you group other views vertically, horizontally or even one on top of another.
Where to go from here?
SwiftUI is still fairly new and evolving as a technology. The best reference is always the official documentation, even though it’s not always generous with descriptions and examples:
- SwiftUI documentation: apple.co/2MlBqJJ
- The
Viewreference documentation apple.co/2LEh5Qs
If you want to take a look and browse through the SF Symbols image library:
- SF Symbols apple.co/2YPtrIx
- SF Symbols App (download) apple.co/30VPAW0
To know more about Markdown, check out:
- Markdown on Wikipedia bit.ly/2VBwiIt
- AttributedString apple.co/3htf3B0
In the next chapter, you’ll learn about other UI components that are commonly used, with particular attention to text fields and buttons.