In this part you will learn about fundamental user inetrface controls.
Table of contents
- Short remark
- Text
- TextField
- SecureField
- TextEditor
- Image
- Label
- Button
- Link
- Toggle
- Slider
- ProgressView
- Stepper
- Picker
- DatePicker
- Contex menu
- Summary
Before you start reading about controls, please read the following remark.
- Remark 1
Sometimes it's really difficult to present the material in such a way as to avoid referring to yet unexplained terms, names etc. In this part I want to focus on user interface controls but in examples sometimes I have to use unknown elements like@State
binding orVStack
. Both I will explain you in subsequent parts and here I give only a one-word explanation but for sure enough to understand what I want to tell you. - Remark 2
In SwiftUI every user interface component belongs toView
family by conforming to theView
protocol. This gives yo a great flexibility to use what SwiftUI has to offer you as well as to make your own user interface components. - Remark 3
You can change the appearance, behaviour, position and interactions of views with modifiers -- some of them you met in previous part, for example those changing how text is displayed:
1234Text("Hello, world!").padding().font(.largeTitle).foregroundColor(.yellow)
What is really nice is that you can define your own modifiers -- a simple example you will see in this part.
Text view is the most basic and obvious component providing simple method to send feedback to the user via text displayed in one or more lines of read-only text.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct ContentView: View { let text = """ line another one line next line """ var body: some View { Text(text) .font(.system(size: 24, weight: .bold, design: .serif)) .italic() .multilineTextAlignment(.center) .foregroundColor(.blue) .opacity(0.7) .lineLimit(2) } } |
What may be strange to you at fist sight is Text
concatenation:
1 2 3 4 5 6 7 8 9 |
struct ContentView: View { var body: some View { Text("line") .font(.system(size: 24, weight: .bold, design: .serif)) .italic() + Text("\n") + Text("another one line") } } |
Text doesn't have to be boring:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct ContentView: View { var body: some View { Text("SwiftUI") .foregroundColor(.white) .font(.title) .padding(35) .background( LinearGradient( colors: [.yellow, .orange], startPoint: .top, endPoint: .bottom ) ) } } |
This control allows you to enter a text. You can customize the appearance and interaction of
TextField
by using TextFieldStyle
instance.
The following example seems to be simple but shows some important informations about views:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
struct ContentView: View { @State var text: String = "" var body: some View { VStack(alignment: .leading) { TextField("Type something here", text: $text) .textFieldStyle(.roundedBorder) .padding() .multilineTextAlignment(.leading) .frame(height:50) Text("Text: ").bold() + Text("\(text)") } } } |
- Inside curly brackets defining
body
there must be only oneView
. If you want to put more views, you have to embed them in "grouping" view likeVStack
in this example. TheVStack
is simply a container that aligns its content vertically. text
property is a state property which automagically controls if value of variable was changed. Every time it occurs, controls using this variable is notified allowing its update to be modiffied. You can observe this either in preview after clicking Lieve preview button button and typing some text or doing the same in simulator. Notice that everything you enter inTextField
immediatelly appears in theText
component below.
As it name states,
SecureField
is a control into which you can securely enter text.
TextEditor
is a view that can display and edit long, multi-line text.
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 |
struct ContentView: View { @State private var text = "" @State private var lastText = "" @State private var totalChars = 0 private let maxChars = 10 var body: some View { VStack(alignment: .leading) { TextEditor(text: $text) .font(.custom("AvenirNext-Regular", size: 20, relativeTo: .body)) .foregroundColor(Color.red) .frame(minWidth: 150, maxWidth: 350, minHeight: 150, maxHeight: 450) .border(Color.blue) .onChange(of: text, perform: { text in totalChars = text.count if totalChars <= maxChars { lastText = text } else { self.text = lastText } }) Text("Chars: \(totalChars) of \(maxChars)") } } } |
Note that if you forget to mark totalChars
with @State
you will get an error
1 |
Cannot assign to property: 'self' is immutable |
1 2 3 4 5 6 7 8 |
struct ContentView: View { var body: some View { Image(systemName: "person.crop.circle") .resizable() .frame(width: 128, height: 128) .foregroundColor(.red) } } |
To load image named foo
from resources use Image
view as follow:
1 |
Image("foo") |
If you are more interested in SF Fonts, please refer to:
- SF Symbols
- How to color your SF Symbols
- SF Symbols Hierarchical, Palette, and Multicolor rendering mode colors?
Label seems to be very simple component but offers you a lot of ways to customize it. In the simplest form you have an icon and text
1 2 3 4 5 |
struct ContentView: View { var body: some View { Label("User", systemImage: "person.crop.circle") } } |
Of course you can apply some modifiers according to your needs:
1 2 3 4 5 6 7 |
struct ContentView: View { var body: some View { Label("User", systemImage: "person.crop.circle") .font(.title) .foregroundColor(.red) } } |
If you prefer to use your own image, replace systemImage
with image
and provide name of it.
You can control how the label is displayed by applying the labelStyle()
modifier:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct ContentView: View { var body: some View { VStack { Label("User", systemImage: "person.crop.circle") .font(.title) .foregroundColor(.red) .labelStyle(.titleAndIcon) Label("User", systemImage: "person.crop.circle") .font(.title) .foregroundColor(.red) .labelStyle(.iconOnly) Label("User", systemImage: "person.crop.circle") .font(.title) .foregroundColor(.red) .labelStyle(.titleOnly) } } } |
You have also an option to return both the icon and title but arranged in a vertical stack:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct LabelStyleVertical: LabelStyle { func makeBody(configuration: Configuration) -> some View { VStack { configuration.icon configuration.title } } } struct ContentView: View { var body: some View { Label("User", systemImage: "person.crop.circle") .font(.title) .foregroundColor(.red) .labelStyle(LabelStyleVertical()) } } |
You can also change the background color:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
struct LabelStyleYellowBackground: LabelStyle { func makeBody(configuration: Configuration) -> some View { Label(configuration) .padding() .background(.yellow) .border(.black, width: 2) } } struct ContentView: View { var body: some View { Label("User", systemImage: "person.crop.circle") .font(.title) .foregroundColor(.red) .labelStyle(LabelStyleYellowBackground()) } } |
You can also create custom title and image closures for more flexibility and control over its appearance:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct ContentView: View { var body: some View { Label { Text("User") .foregroundColor(.red) } icon: { Image(systemName: "person.crop.circle") .font(.title) .foregroundColor(Color.blue) } } } |
Going further, you can use a shape in place of label’s image:
1 2 3 4 5 6 7 8 9 10 11 12 |
struct ContentView: View { var body: some View { Label { Text("User") .foregroundColor(.red) } icon: { Capsule() .frame(width: 32, height: 16) .foregroundColor(Color.blue) } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct ContentView: View { var body: some View { Label { Text("User") .padding(7) .foregroundColor(.black) .background(.yellow) .clipShape(Capsule()) } icon: { Capsule() .frame(width: 32, height: 16) .foregroundColor(Color.blue) } } } |
Main purpose of button is to trigger an action:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct ContentView: View { @State private var counter = 0 var body: some View { VStack(alignment: .leading) { Button("Tap Me") { counter += 1 } .padding() Text("Tap counter: ").bold() + Text("\(counter)") } } } |
When you tap the button three times you will see:
You can go beyond daily routine and easily create really nice buttons:
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 |
struct ContentView: View { @State private var counter = 0 var body: some View { VStack(alignment: .leading) { Button( action: { counter += 1 }, label: { Label { Text("User") .foregroundColor(.indigo) .bold() } icon: { Image(systemName: "person.crop.circle.badge.plus") .symbolRenderingMode(.palette) .foregroundStyle(.green, .pink) .font(.largeTitle) }.padding() } ) .frame(width:300, height:100) .background(Color.blue.opacity(0.2)) .cornerRadius(20) Text("User counter: ").bold() + Text("\(counter)") } } } |
When you tap the button three times you will see:
Another one inspiration for you:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
struct ContentView: View { @State private var counter = 0 var body: some View { VStack(alignment: .leading) { Button(action: { counter += 1 }, label: { Image(systemName: "person.crop.circle.badge.plus").font(.largeTitle) VStack(alignment: .leading) { Text("Tap Me").font(.largeTitle) Text("To increase counter") } }) .foregroundColor(Color.white) .padding() .background(Color.blue) .cornerRadius(5) Text("User counter: ").bold() + Text("\(counter)") } } } |
When you tap the button three times you will see:
1 2 3 4 5 6 |
struct ContentView: View { var body: some View { Link("Piotr Fulmański homepage", destination: URL(string: "https://fulmanski.pl")!) } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
struct ContentView: View { @State private var isSound = true var body: some View { VStack(alignment: .leading) { Toggle("Sound", isOn: $isSound) VStack(alignment: .center) { Toggle(isOn: $isSound){ Text("Sound") .font(.largeTitle) } Image(systemName: isSound ? "speaker.1.fill":"speaker.slash.fill") .font(.largeTitle) .foregroundColor(isSound ? .green : .red) } .padding() } } } |
Slider allows you to select a value from a bounded linear range of values:
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { @State var value: Float = 0 var body: some View { VStack(alignment: .center) { Slider(value: $value, in: -10...10) Text("\(value)") }.padding() } } |
1 2 3 4 5 6 7 8 9 10 |
struct ContentView: View { @State var value: Float = 0 var body: some View { VStack(alignment: .center) { Slider(value: $value, in: -10...10, step: 1) Text("\(value)") }.padding() } } |
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 |
struct ContentView: View { @State var value: Double = 0 var body: some View { VStack(alignment: .center) { HStack { Image(systemName: "thermometer.snowflake").font(.largeTitle) Slider(value: $value, in: -30...30) .accentColor(getColor(value: value)) Image(systemName: "thermometer.sun").font(.largeTitle) } Text("\(value, specifier: "%.2f")") }.padding() } func getColor(value: Double) -> Color { if value < -20 { return Color.purple } else if value < -10 { return Color.blue } else if value < 0 { return Color.teal } else if value == 0 { return Color.gray.opacity(0.2) } else if value < 10 { return Color.yellow } else if value < 20 { return Color.orange } else if value <= 30 { return Color.red } return Color.black } } |
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 |
struct ContentView: View { @State private var progress = 0.0 var body: some View { VStack(alignment: .center) { VStack { ProgressView("Please wait...", value: progress, total: 10) Button("Make progress", action: { progress += 2.0 }) } ProgressView("Please wait...") .progressViewStyle(CircularProgressViewStyle()) ProgressView { Button(action: { // Do something to stop the task. }) { Text("Cancel") .foregroundColor(.white) } .padding(8) .background(Color.red) .cornerRadius(5) } }.padding() } } |
For custom progress view you can read following materials:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
struct ContentView: View { @State var value1: Double = 0 @State var value2: Double = 0 @State var value3: Double = 0 @State var value4: Double = 1 var body: some View { VStack(alignment: .center) { Stepper("\(value1, specifier: "%.2f")", value: $value1) Stepper("\(value2, specifier: "%.2f")", value: $value2, in: 0...10) Stepper("\(value3, specifier: "%.2f")", value: $value3, step: 2) Stepper(onIncrement: { self.value4 *= 2 }, onDecrement: { self.value4 /= 2 }, label: { Text("\(value4, specifier: "%.2f")") }) }.padding() } } |
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 |
struct ContentView: View { @State var choice = 0 var settings = ["one", "two", "three", "four"] var body: some View { VStack(alignment: .center) { Picker(selection: .constant(1), label: Text("Options"), content: { Text("one").tag(0) Text("two").tag(1) Text("three").tag(2) Text("four").tag(3) }) Picker("Options", selection: $choice) { ForEach(0 ..< 4) { index in Text(self.settings[index]) .tag(index) } } Picker(selection: $choice, label: Text("Options"), content: { Text("one").tag(0) Text("two").tag(1) Text("three").tag(2) Text("four").tag(3) }) .pickerStyle(SegmentedPickerStyle()) }.padding() } } |
Notice usage of .constant(1)
in place of variable. It creates a binding with an immutable value and in most cases is used as a dummy variable when you want to test UI but not want to create required variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
struct ContentView: View { @State private var date = Date() var body: some View { VStack(alignment: .center) { DatePicker("Date:", selection: $date) DatePicker("Date:", selection: $date, in: Date()... ) DatePicker("Date:", selection: $date, displayedComponents: [.date] ) }.padding() } } |
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 |
struct ContentView: View { var body: some View { Text("Show context menu") .font(.title) .fontWeight(.bold) .foregroundColor(Color.orange) .contextMenu { Button(action: { // Action }) { Text("Delete") Image(systemName: "trash") } Button(action: { // action }) { Text("Add") Image(systemName: "plus") } Button(action: { // Action }) { Text("Share") Image(systemName: "square.and.arrow.up") } Button(action: { // action }) { Text("Favorite") Image(systemName: "heart.fill") } } } } |
Remember not to tap on text but rather tap and hold your finger on it until context menu will apear.
After reading and practising this part you have an overview of most common user interface components. You know also how to style them and even can do simple ineraction among them. Keep reading next part where you will learn how to lay them out and combine to obtain one consistent user interface.