6.
Controls & User Input
Written by Antonio Bello
In Chapter 5, “Intro to Controls: Text & Image” you learned how to use two of the most commonly used controls: Text and Image, with also a brief look at Label, which combines both controls into one.
In this chapter, you’ll learn more about other commonly-used controls for user input, such as TextField, Button and Stepper and more, as well as the power of refactoring.
A simple registration form
The Welcome to Kuchi screen you implemented in Chapter 5 was good to get you started with Text and Image, and to get your feet wet with modifiers. Now, you’re going to add some interactivity to the app by implementing a simple form to ask the user to enter her name.
The starter project for this chapter is nearly identical to the final one from Chapter 5 — that’s right, you’ll start from where you left off. The only difference is that you’ll find some new files included needed to get your work done for this chapter.
If you prefer to keep working on your own copy of the project borrowed from the previous chapter, feel free to do so, but in this case copy and manually add to both iOS and macOS targets the additional files needed in this chapter from the starter project:
- Shared/Profile/Profile.swift
- Shared/Profile/Settings.swift
- Shared/Profile/UserManager.swift
To do so, it’s better if you just add the Shared/Profile folder, so that Xcode can create the Profile group, and automatically add all files in it contained without any extra step. To do so:
- In the Project navigator right click on the Shared group.
- Choose Add files to “Kuchi” in the dropdown menu.
- Make sure that both iOS and macOS targets are selected.
- Select the Profile folder and click Add.
You will use these new files later in this chapter — but feel free to take a look.
A bit of refactoring
Often, you’ll need to refactor your work to make it more reusable and to minimize the amount of code you write for each view. This is a pattern that’s used frequently and often recommended by Apple.
The new registration view you will be building will have the same background image as the welcome view you created in Chapter 5. Here’s the first case where refactoring will come in handy. You could simply copy code from the welcome view and paste it, but that’s not very reusable and maintainable, is it?
First of all, create a Components group in the Project navigator by right-clicking on the Shared/Welcome group and choosing New Group.
Then, create a new component view by right-clicking the Components group, and creating a new SwiftUI View named WelcomeBackgroundImage — again, be sure to add to both targets, iOS and macOS.
Next, open WelcomeView, select the following lines of code, which define the background image, and copy them:
Image("welcome-background")
.resizable()
.aspectRatio(1 / 1, contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.saturation(0.5)
.blur(radius: 5)
.opacity(0.08)
Then, paste in the body implementation of WelcomeBackgroundImage the code you’ve copied above (replacing the default Text it contains). The body should now look as follows:
var body: some View {
Image("welcome-background")
.resizable()
.aspectRatio(1 / 1, contentMode: .fill)
.edgesIgnoringSafeArea(.all)
.saturation(0.5)
.blur(radius: 5)
.opacity(0.08)
}
Now, go back to WelcomeView and replace the lines of code you previously copied with the newly created view, so that it looks like this:
var body: some View {
ZStack {
WelcomeBackgroundImage()
Label {
...
Make sure that you’ve enabled automatic preview (resume it if necessary), and you’ll notice that nothing has changed, which is what you’d expect — because you refactored your code without making any functional changes.
Since the topic of this section is refactoring, you’ll go a step further and refactor:
- The icon image that’s displayed in the welcome view.
- The entire Welcome view, composed by the icon and the “Welcome to Kuchi” text.
Exercise: Now that you’ve unlocked the SwiftUI refactoring ninja achievement, why don’t you try to do the two refactoring on your own, and then compare your work with how it’s been done below? You can name the two new views
LogoImageandWelcomeMessageView.
Refactoring the logo image
In WelcomeView.swift select the code for 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)
Then:
- Copy the code to your clipboard.
- Replace the code with
LogoImage(). - Create a new LogoImage.swift file in the Components group, using the SwiftUI template.
- Replace the
bodyimplementation of LogoImage with the code you’ve copied from the welcome view.
If you open WelcomeView and resume the preview, once again you won’t notice any differences — which means the refactoring worked.
Refactoring the welcome message
In WelcomeView, you’ll do this a bit differently:
- Command-Click on
Label. A popup menu will appear:
- Choose Extract Subview. Xcode will replace the selected component with
ExtractedView(), and will move its implementation at the end of the file, in a newExtractedViewstruct.
- If Xcode is not so kind to put the new view name in edit mode, right click on
ExtractedViewand choose Refactor and then Rename. - Type a new name in — Call it
WelcomeMessageViewand press Enter. - Now you’re going to move it to a new file. Select the entire
WelcomeMessageViewstruct and cut it. - Next, create a new WelcomeMessageView file in the Components group, using the SwiftUI template.
- Replace the implementation of
WelcomeMessageViewwith the code you’ve cut from the welcome view.
Once again, if you open WelcomeView and resume the preview, you won’t notice any difference.
Good job! You’ve just refactored the welcome view making it, and the components it consists of, much more reusable.
Creating the registration view
The new registration view is… well, new, so you’ll have to create a file for it. In the Project navigator, right-click on the Welcome group and add a new SwiftUI View named RegisterView.
Next, replace its body implementation with:
VStack {
WelcomeMessageView()
}
And with a single line of code, you’ve just proved how easy and powerful a reusable small components can be.
You can also add a background view, which, thanks to the previous refactoring, is as simple as adding a couple lines of code. Replace the body implementation with this code:
ZStack {
WelcomeBackgroundImage()
VStack {
WelcomeMessageView()
}
}
Voilà, lunch is served. Faster than a microwave!
If you try to run the app, you’ll notice it still displays the welcome view. Well, probably you won’t notice that easily, because the two views look exactly the same. But that’s not the point. :]
Anyway, the app is still configured to display the welcome view on launch. To change that, open KuchiApp and replace WelcomeView with RegisterView:
var body: some Scene {
WindowGroup {
RegisterView()
}
}
And do the same to the preview, so that it looks like:
struct KuchiApp_Previews: PreviewProvider {
static var previews: some View {
RegisterView()
}
}
Power to the user: the TextField
With the refactoring done, you can now focus on giving the user a way to enter her name into the app.
In the previous section, you added a VStack container to RegisterView, and that wasn’t a random decision, because you need it now to stack content vertically.
TextField is the control you use to let the user enter data, usually by way of the keyboard. If you’ve built an iOS or macOS app before, you’ve probably met its older cousins, UITextField and NSTextField.
In its simplest form, you can add the control using the initializer that takes a title and a text binding.
The title is the placeholder text that appears inside the text field when it is empty, whereas the binding is the managed property that takes care of the 2-way connection between the text field’s text and the property itself.
You will learn more about binding in Chapter 8: “State & Data Flow — Part I”. For now you just need to know that to create and use a binding you have to:
- Add the
@Stateattribute to a property. - Prefix the property with
$to pass the binding instead of the property value.
So, add this property to RegisterView:
@State var name: String = ""
And then add the text field after WelcomeMessageView():
TextField("Type your name...", text: $name)
You’d expect a text field to appear in the preview, but nothing happens — it looks the same as before. What gives?
A closer inspection reveals the problem: if you click TextField in the code editor, you’ll notice that the text field gets selected in the preview — it’s just that it’s too wide, as you can see from the blue rectangle:
Challenge: can you figure out why is this happening? Hint: it’s caused by the background image.
The reason is that the background image is configured with .fill content mode, which means that the image expands to occupy as much of the parent view space as possible. Because the image is a square, it fits the parent vertically, but that means that, horizontally, it goes way beyond the screen boundaries.
The way to fix this is to avoid using a ZStack and to position the background view behind the actual content using the .background modifier on the VStack instead.
Remove the ZStack from the register view, and then add WelcomeBackgroundImage() as a .background modifier to the VStack:
var body: some View {
VStack {
WelcomeMessageView()
TextField("Type your name...", text: $name)
}
.background(WelcomeBackgroundImage())
}
Note: In UIKit, views have a
backgroundColorproperty, which you can use to specify a uniform background color. The SwiftUI counterpart is more polymorphic; the.backgroundmodifier accepts any type that conforms toView, which includesColor,Image,Shape, among others.
With this change, the text field is now visible, but the background looks too small.
The reason is that VStack is not using the entire screen, but only what it needs to render its content. In the picture above you can see its actual size, highlighted in blue.
To fix this problem, add two Spacers, one at the beginning and the other at the end of VStack, as follows:
VStack {
Spacer() // <-- 1st spacer to add
WelcomeMessageView()
TextField("Type your name...", text: $name)
Spacer() // <-- 2nd spacer to add
} .background(WelcomeBackgroundImage())
You’ll know more about Spacer in the next chapter, what you need to know for now is that it expands in a way to use all space at its disposal. With this change, now the background images expand as expected.
Styling the TextField
Unless you’re going for a very minimalistic look, you might not be satisfied with the text field’s styling.
To make it look better, you need to add some padding and a border. For the border, you can take advantage of the .textFieldStyle modifier, which applies a style to the text field.
Currently, SwiftUI provides four different styles, which are compared in the image below:
The “no style” case is explicitly mentioned, but it corresponds to DefaultTextFieldStyle. You can see that there’s no noticeable difference between DefaultTextFieldStyle and PlainTextFieldStyle. However, RoundedBorderTextFieldStyle presents a border with slightly rounded corners. Note that there’s also a fifth style, SquareBorderTextFieldStyle, but it’s available on macOS only.
For Kuchi, you’re going to provide a different, custom style. There are three options for this:
- Apply modifiers to the
TextFieldas needed. - Create your own text field style, by defining a concrete type conforming to the
TextFieldStyleprotocol. - Create a custom modifier, by defining a concrete type conforming to the
ViewModifierprotocol.
Whichever solution you choose, it consists of directly or indirectly applying a list of modifiers in sequence, one after the other, so the most logical way to start is with the first method.
Apply the following modifiers to the text field:
.padding(
EdgeInsets(
top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 2)
.foregroundColor(.blue)
)
.shadow(
color: Color.gray.opacity(0.4),
radius: 3, x: 1, y: 2)
The figure below shows the effect of each modifier:
This is what each does:
- Creates an unmodified text field.
- Adds padding of 16 points vertically, and 8 points horizontally.
- Adds a non-transparent white background.
- Creates an overlay for the border, using a rounded rectangle with a corner radius of 8.
- Adds a stroke effect to keep the border only, leaving the content behind visible.
- Makes the border blue.
- Adds a shadow.
You’ll notice that the text field has no spacing from the left and right edges; the padding you added in Step 2 adds padding between the text field and the views it contains. To add padding between the text field and its parent view, you’ll need to add a padding modifier to the view that contains the text field, the VStack.
In the containing VStack, right before .background(WelcomeBackgroundImage()), but after the stack’s closing bracket, add the following:
.padding()
Creating a custom text style
Now that you have a list of modifiers applied to the text field which provide a style you like, you can convert this list into a custom text style, so that you can declare it once and reuse every time you need it.
A custom text field style must adopt the TextFieldStyle, which declares one method only:
public func _body(
configuration: TextField<Self._Label>) -> some View
It receives the text field in the configuration parameters, to which you can apply as many modifiers as you want, returning the resulting view.
In RegisterView, before the RegisterView struct, create a new custom text style:
struct KuchiTextStyle: TextFieldStyle {
public func _body(
configuration: TextField<Self._Label>) -> some View {
return configuration
}
}
Left as is, this text style doesn’t do anything, because it returns the same text field it receives. To customize it, you need to add modifiers.
So, move the four modifiers you applied earlier to the text field to this method. In RegisterView select and cut these lines:
.padding(
EdgeInsets(
top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 2)
.foregroundColor(.blue)
)
.shadow(color: Color.gray.opacity(0.4),
radius: 3, x: 1, y: 2)
and paste them into the KuchiTextStyle’s body implementation, after the return configuration statement, so that it looks like:
public func _body(
configuration: TextField<Self._Label>) -> some View {
return configuration
.padding(
EdgeInsets(
top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 2)
.foregroundColor(.blue)
)
.shadow(color: Color.gray.opacity(0.4),
radius: 3, x: 1, y: 2)
}
What you are returning is the resulting text field after applying the four modifiers.
Now you can use this new style. Head back to RegisterView, and add it to the text field, using the textFieldStyle modifier, so that it looks like:
TextField("Type your name...", text: $name)
.textFieldStyle(KuchiTextStyle())
Here you create a new instance of KuchiTextStyle, and pass it to the textFieldStyle. Simple!
If you look at the preview, you’ll see the same as before refactoring — nothing has changed from a functional standpoint — which is expected.
Now, you don’t need this custom style anymore, because in the next section you’ll go for the custom modifier path. Undo all the changes (pressing Control + Z repeatedly) until you see the 4 modifiers applied again to the text field, and the newly created KuchiTextStyle gone — verify that the RegisterView’s body implementation is:
var body: some View {
VStack {
Spacer()
WelcomeMessageView()
TextField("Type your name...", text: $name)
.padding(
EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 2)
.foregroundColor(.blue)
)
.shadow(color: Color.gray.opacity(0.4),
radius: 3, x: 1, y: 2)
Spacer()
}
.padding()
.background(WelcomeBackgroundImage())
}
Creating a custom modifier
The reason for preferring the custom modifier over the custom text field style is that you can apply the same modifier to any view, including buttons — which, spoiler alert, is what you’re going to do soon.
Add a new file to the Components group using the SwiftUI View template, and name it BorderedViewModifier.
First, delete the autogenerated BorderedViewModifier_Previews struct, as you don’t need it for a custom modifier. Next, change the protocol that BorderedViewModifier conforms to, from View to ViewModifier:
struct BorderedViewModifier: ViewModifier {
A ViewModifier defines a body member, but instead of being a property, it’s a function that takes content — the view the modifier is applied to — and returns another view resulting from the modifier being applied to the content. You see a recurring pattern because it’s conceptually similar to the custom text field style.
Replace the property with the following function:
func body(content: Content) -> some View {
content
}
The code, as is, returns the same view the modifier is applied to. Don’t worry, you’re not done yet! :]
Go back to RegisterView, then select and cut again all modifiers applied to the text field:
.padding(
EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 2)
.foregroundColor(.blue)
)
.shadow(color: Color.gray.opacity(0.4),
radius: 3, x: 1, y: 2)
Next, switch back to BorderedViewModifier and paste these modifiers after content:
func body(content: Content) -> some View {
content
.padding(
EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
.background(Color.white)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(lineWidth: 2)
.foregroundColor(.blue)
)
.shadow(color: Color.gray.opacity(0.4),
radius: 3, x: 1, y: 2)
}
That’s it. Now you have a new custom modifier. To apply it, you need to create an instance of ModifiedContent, a struct that comes with SwiftUI. Its initializer takes two parameters:
- The content view
- The modifier
Reopen RegisterView, and embed the TextField in a ModifiedContent instance, as follows:
ModifiedContent(
content: TextField("Type your name...", text: $name),
modifier: BorderedViewModifier()
)
After the preview updates, you see that the blue border is correctly applied. But hey, let’s be honest, that code doesn’t look fantastic. Wouldn’t it be better if you could replace it with a simpler modifier call, like any regular modifier call?
Turns out all you need to do is create a convenience method in a view extension. Open BorderedViewModifier, and add the following extension at the end of the file:
extension View {
func bordered() -> some View {
ModifiedContent(
content: self,
modifier: BorderedViewModifier()
)
}
}
Now, you can go back to RegisterView and replace the ModifiedContent component with the following:
TextField("Type your name...", text: $name)
.bordered()
The preview will confirm that the modifier is correctly applied — and, you’ve already guessed, what you see is the same as before, because, again, you haven’t applied any functional change, just code refactoring.
Keyboard and Form Tuning
If you run the app (making sure that the soft keyboard is enabled if you’re using the simulator) you notice that when you tap the text field the keyboard is automatically displayed, and the layout automatically adjusted to make sure that the text field is visible and not covered by the keyboard itself.
The action key on the keyboard is labelled return, but in some cases you want to change it, so that it displays next if you have more fields you want to move the focus to, or done when the field is the last of the form.
You can change the label of the action button in a very easy way, thanks to, you guessed, a TextField’s modifier. Open RegisterView.swift and right after the text field, but before the .bordered() modifier, add this line:
.submitLabel(.done)
This instructs the keyboard to show the text associated to the done action. You can’t specify a custom label though: you are limited to the enum cases of the SubmitLabel type: done, go, send, join, route, search, return, next and continue.
Note that changing the label won’t alter the key’s behavior — when you press the done button, the keyboard will be dismissed as it did before the change. However you can associate an action — more on that in the next section: “Taps and buttons”.
Another new addition to SwiftUI 3 is the ability to specify and know at any time which control has the focus. To achieve that, you need to add a property to the view - it can be of any type, as long as it conforms to the Hashable protocol.
Spoiler alert: you will undo the changes you’re doing now, to implement at the end of this same section an alternative version achieving the same result.
The most natural way to handle focus is by using an enum, with a case for each control that can obtain the focus — in the case of this RegisterView there’s one field only to enter the user’s name. So define an enum inside the RegisterView struct:
struct RegisterView: View {
// Add this enum
enum Field: Hashable {
case name
}
...
}
Next, add a property to the view, using the @FocusState attribute after userManager:
@FocusState var focusedField: Field?
Last, you need to create an association between the enum case and the text field — this needs to be a two way binding, so that:
- When an enum case is assigned to
focusedField, the associated component will get the focus. - When a component obtains the focus (in response to a user’s action), the
focusedFieldproperty will be set to its corresponding enum case.
The binding is done using, you guessed again, a modifier, which takes a focus state property binding, plus a value which determines the value associated to the component. Add the .focused modifier to the text field as follows:
TextField("Type your name...", text: $userManager.profile.name)
// Add this modifier
.focused($focusedField, equals: .name)
.submitLabel(.done)
.bordered()
With this modifier you’re telling SwiftUI:
- When
focusFieldis.name, give this text field focus. - When this field gets focus, set
focusFieldto.name.
Now, if you run the app, you won’t notice any difference — that’s because you’ve created the binding, but you’re not using yet. You could think of initializing the field with a default value, so that a field has focus when the view is displayed, but that’s considered an anti-pattern, and it won’t have any effect — feel free to try it.
What you can do in this simple form is to remove the focus from the text field when the OK button is tapped. You’ll do that later in this chapter.
However, in this form you have one field only, so using an enum is a bit overkill, don’t you agree? The Apple Engineers have thought about that, and implemented an alternative way that relies on booleans rather than enums.
The idea is to bind a component to a boolean property. If you have multiple components, you need a dedicated property for each component.
Since this solution is a better fit in the current scenario, let’s change the implementation to use it:
-
First of all, delete the
Fieldenum altogether. -
Next, replace the
focusFieldproperty with this new implementation:@FocusState var nameFieldFocused: BoolThe new property is a boolean, so it can be either
trueorfalse, reflecting the current focus state of the bound component, which can be with focus or without focus. -
Last, use a different overload of the
.focused()modifier applied to the text field:.focused($nameFieldFocused)The binding created with the
.focusedmodifier needs just the focus state property - no value to compare with is needed (as it was for the enum based variant) because, as mentioned, the property value can be either true or false.Said in a different way:
- Using the enum variant, the property tells which field has focus.
- Using the boolean variant, the property tells if the associated field has focus or not.
As anticipated a few lines above, you’ll use the focus later in this chapter, when discussing about submitting the form.
A peek at TextField’s initializer
TextField has several initializers, many available in pairs, with each pair having a localized and non-localized version for the title parameter.
The version used in this chapter is the non-localized version that takes a title and a binding for the editable text:
public init<S>(
_ title: S,
text: Binding<String>,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}
) where S : StringProtocol
There are two parameters that you haven’t used here, and further, that you haven’t explicitly provided as they have empty implementation by default. These parameters are two closures that can be used to perform additional processing before and after the user input:
-
onEditingChanged: Called when the edit obtains focus (when the Boolean parameter istrue) or loses focus (when the parameter isfalse). -
onCommit: Called when the user performs a commit action, such as pressing the return key. This is useful when you want to handle moving the focus to the next field automatically.
Another pair of initializers take an additional formatter. The non localized version has this signature:
public init<S, T>(
_ title: S,
value: Binding<T>,
formatter: Formatter,
onEditingChanged: @escaping (Bool) -> Void = { _ in },
onCommit: @escaping () -> Void = {}
) where S : StringProtocol
The differences from the other pair are such:
-
The
formatterparameter, which is an instance of a class inherited fromFoundation’s abstract classFormatter. It’s usable when the edited value is of a different type than String — for instance, a number or a date — but you can also create custom formatters. -
The
Tgeneric parameter determines the actual underlying type handled by the TextField.
For more information about formatters, take a look at Data Formatting apple.co/2MNqO7q.
New to SwiftUI 3, there’s also a new pair of parameters that you can pass to the initializer:
-
promptlets you pass aTextinstance that will be used for the placeholder text — The difference compared to thetitleparameter is that you can apply custom formatting, such as changing font. -
labellets you pass aViewwhich describes the purpose of the text field.
These two parameters are used in different ways depending on the platform where the app runs:
- On macOS the label is displayed next to the leading edge of the text field, and the prompt as the placeholder text.
- On iOS the label will be used as placeholder, if provided, otherwise the prompt will be used.
Taps and buttons
Now that you’ve got a form, the most natural thing you’d want your user to do is to submit the form. And the most natural way of doing that is using a dear old submit button.
The SwiftUI button is far more flexible than its UIKit/AppKit counterpart. You aren’t limited to using a text label alone or in combination with an image for its content.
Instead, you can use anything for your button that’s a View. You can see this from its declaration, which makes use of a generic type:
struct Button<Label> where Label : View
The generic type is the button’s visual content, which must conform to View.
That means a button can contain not only a base component, such as a Text or an Image, but also any composite component, such as a pair of Text and Image controls, enclosed in a vertical or horizontal stack, or even anything more complex that you can dream up.
Adding a button is as easy as declaring it: you simply specify a label and attach a handler. Its signature is:
init(
action: @escaping () -> Void,
@ViewBuilder label: () -> Label
)
The initializer takes two parameters, which are actually two closures:
- action: the trigger handler
- label: the button content
The @ViewBuilder attribute applied to the label parameter is used to let the closure return multiple child views.
Note: The tap handler parameter is referred to as action instead of tap or tapAction — and if you read the documentation, it’s called a trigger handler, not tap handler.
That’s because in iOS it’s a tap, but in macOS it can be a mouse click, in watchOS a digital crown press, and so forth.
Note: The button initializer takes the trigger handler as the first parameter, instead of the last, breaking the common practice in Swift of giving action closures the last position.
This means that you can’t use the single trailing closure syntax. The reason is very likely because that pattern changes in SwiftUI, where the last parameter is always the view declaration — which, by the way, can use the same trailing closure syntax. However you can always use the multiple trailing closure syntax, new to Swift 5.3.
Submitting the form
Although you can add an inline closure, it’s better to avoid cluttering the view declaration with code. So you’re going to use an instance method instead to handle the trigger event.
In RegisterView add the button after the TextField:
Button(action: registerUser) {
Text("OK")
}
Then, after the RegisterView struct, add this extension, containing the registerUser() event handler:
// MARK: - Event Handlers
extension RegisterView {
func registerUser() {
print("Button triggered")
}
}
Now run the app in the Simulator and when you press OK a message will be printed to the Xcode console.
Now that the trigger handler is wired up, you should do something more useful than printing a message to the console. The project comes with a UserManager class that takes care of saving and restoring a user and the user settings respectively to and from the user defaults.
UserManager conforms to ObservableObject, a protocol that enables the class to be used in views. It triggers a view update when the instance state changes. This class exposes two properties — profile and settings – marked with the @Published attribute, which identifies the state that triggers view reloads.
That said, you can delete the name property in RegisterView, and replace with an instance of UserManager:
@EnvironmentObject var userManager: UserManager
It’s marked with the @EnvironmentObject attribute because you’re going to inject an instance of it once for the whole app, and retrieve it from the environment anywhere it is needed. You will learn more about ObservableObject and @EnvironmentObject in Chapter 9: “State & Data Flow — Part II”.
Next, in the TextField, you have to change the $name reference to $userManager.profile.name, so that it looks like the following:
TextField("Type your name...", text: $userManager.profile.name)
.submitLabel(.done)
.bordered()
Lastly, in registerUser() replace the print statement with this more useful implementation:
func registerUser() {
userManager.persistProfile()
}
Now, if you try to preview this view, it will fail. That’s because, as mentioned above, an instance of UserManager should be injected. You do this in the RegisterView_Previews struct, by passing a user manager to the view via a .environmentObject modifier. Update the RegisterView_Previews implementation so that it looks like this:
struct RegisterView_Previews: PreviewProvider {
static let user = UserManager(name: "Ray")
static var previews: some View {
RegisterView()
.environmentObject(user)
}
}
Likewise, if you run the app in the Simulator, it will crash. The change you’ve just made is only for the preview, and it doesn’t affect the app. You need to makes changes in KuchiApp as well. Open it and add this property and initializer to KuchiApp:
let userManager = UserManager()
init() {
userManager.load()
}
This creates an instance of UserManager, and makes sure the stored user, if available, is loaded. Next, use the environmentObject modifier on the RegisterView instance to inject it:
var body: some Scene {
WindowGroup {
RegisterView()
// Add this line
.environmentObject(userManager)
}
}
Last, you also need to update the app preview, which will crash if you don’t provide the user manager like you did for RegisterView_Previews. Scroll down to the end of the file and replace the whole KuchiApp_Previews with:
struct KuchiApp_Previews: PreviewProvider {
static let userManager = UserManager(name: "Ray")
static var previews: some View {
RegisterView()
.environmentObject(userManager)
}
}
Styling the button
The button is fully operative now; it looks good, but not great. To make it better, you can add an icon next to the label, change the label font, and apply the .bordered() modifier you created for the TextField earlier.
In RegisterView.swift, locate the button, and replace it with this code:
Button(action: self.registerUser) {
// 1
HStack {
// 2
Image(systemName: "checkmark")
// 3
.resizable()
.frame(width: 16, height: 16, alignment: .center)
Text("OK")
// 4
.font(.body)
.bold()
}
}
// 5
.bordered()
You should already be able to discern what this code does, but here’s a breakdown:
- As previously stated, the
labelparameter can return multiple child views, but here you’re using a horizontal stack to group views horizontally. If you omit this, the two components will be laid out vertically instead. - You add a checkmark icon.
- You make the icon resizable, centered, and with fixed 16×16 size. You need to use
.resizable()because otherwise the image would keep its original size, and ignore the size of the view it is contained in. - You change the label font, specifying a
.bodytype and a bold weight. - You apply the
.borderedmodifier that you’ve created earlier, to add a blue border with rounded corners.
If you did everything correctly, this is what your preview should look like:
New to SwiftUI 3.0, you can also use a style thanks to the new .buttonStyle(_:) modifier, which accepts an instance of a type conforming to the PrimitiveButtonStyle protocol.
There’s a list of predefined styles with which you can immediately use, such as bordered, borderedProminent, borderless, card, link and plain (note that each platform uses a subset of them, so for example link and card are not available in iOS).
And, in case you’re wondering, you can also create your own style — in fact that modifier just needs an object that conforms to PrimitiveButtonStyle, so it works in a similar way to how custom styles are created for text fields, as you briefly saw in the previous chapter.
Reacting to input: validation
Now that you’ve added a button to submit the form, the next step in a reactive user interface is to react to the user input while the user is entering it.
It might be quite useful for different reasons, such as:
- Validating the data while it is entered
- Showing a counter of the number of characters typed in
But the list doesn’t end there. The old way of monitoring the input entered by the user in UIKit was either by way of a delegate or subscribing to a Notification Center event. You’re likely tempted to look for a similar way to react to input changes, such as a modifier that takes a handler closure, which is called every time the user presses a key.
However, the SwiftUI way to monitor for input changes is different.
Say you want to validate the user input, and keep the OK button disabled until the input is valid. In the old days, you’d subscribe for a value changed event, perform a logical expression to determine whether to enable or disable the button, and then update the button state.
The difference in SwiftUI is that you pass the logical expression to a button’s modifier, and… there is no “and”. That’s all. When a status change occurs, the view is rerendered, the logical expression is re-evaluated, and the button’s disabled status is updated.
In RegisterView, add this modifier to the OK button:
.disabled(!userManager.isUserNameValid())
This modifier changes the disabled state. It belongs to the View protocol, so it applies to any view. It takes one parameter only: a Boolean stating whether the view is interactable or not.
When the user types in the TextField, the userManager.profile.name property changes, and that triggers a view update. So, when the button is rerendered, the expression in .disabled() is re-evaluated, and therefore the button state is automatically updated when the input changes.
In this app, the requirement for a name is that it has to be at least three characters long — and this is what isUserNameValid() checks. Now you can run the app and edit the name: you’ll notice that if the name length is less than 3, the button gets disabled, and it’s enabled again as soon as you type the 3rd character in.
Reacting to input: counting characters
If you’d want to add a label showing the number of characters entered by the user, the process is very similar. After the TextField, add this code:
HStack {
// 1
Spacer()
// 2
Text("\(userManager.profile.name.count)")
.font(.caption)
// 3
.foregroundColor(
userManager.isUserNameValid() ? .green : .red)
.padding(.trailing)
}
// 4
.padding(.bottom)
Going over this line-by-line:
- You use a spacer to push the Text to the right, in a pseudo-right-alignment way.
- This is a simple
Textcontrol, whose text is the count of characters of thenameproperty. - You use a green text color if the input passes validation, red otherwise.
- This adds some spacing from the OK button.
You can now run the app, or enable live preview in Xcode, to see the counter in action. As you type, it will display the number of entered characters, using a green number, unless the count is less than 3, in which case it will turn red.
Toggle Control
Next up: a new component. The toggle is a Boolean control that can have an on or off state. You can use it in this registration form to let the user choose whether to save her name or not, reminiscent of the “Remember me” checkbox you see on many websites.
The Toggle initializer is similar to the one used for the TextField. Its initializer takes a binding and a label view:
public init(
isOn: Binding<Bool>,
@ViewBuilder label: () -> Label
)
For the binding, although you could use a state property owned by RegisterView, it’s better to store it in a place that can be accessed from other views. The UserManager class already defines a settings property dedicated to that purpose.
After the HStack you added earlier for the name counter, and before the Button, add the following code:
HStack {
// 1
Spacer()
// 2
Toggle(isOn: $userManager.settings.rememberUser) {
// 3
Text("Remember me")
// 4
.font(.subheadline)
.foregroundColor(.gray)
}
// 5
.fixedSize()
}
The code is very simple and straightforward:
- You need the spacer to add flexible spacing to the left, to push the toggle toward the right, and make it right-aligned.
- You create the
Togglecomponent, binding to$userManager.settings.rememberUser. - This is the label displayed before the component itself.
- You alter the default style of the label to make it smaller and gray.
- You ask the toggle to choose its ideal size. Without it, the toggle will try to expand horizontally, taking all the available space.
This change alone won’t actually add anything functional to the app, besides storing the toggle state as a property. Replace the implementation of registerUser() with:
func registerUser() {
// 1
if userManager.settings.rememberUser {
// 2
userManager.persistProfile()
} else {
// 3
userManager.clear()
}
// 4
userManager.persistSettings()
userManager.setRegistered()
}
In this updated version:
- You check if the user chose whether to remember herself or not.
- If yes, then make the profile persistent.
- Otherwise, clear the user defaults.
- Finally, store the settings and mark the user as registered.
To see this in effect, you need to run the app. The first time you run it, no user profile will be stored. Enter a name, enable the “Remember me” toggle, and press OK; the next time you launch the app, it will prefill the TextField with the name you entered.
Handling the Focus and the Keyboard
Now that everything is wired up, and the buttons correctly handles the tap, let’s get back to the focus. The form implemented in this registration view is very simple, so there’s no advanced use of focus management, but there’s one thing you can do to improve the user experience.
If you run the app in the simulator, and you tap the text field to edit the user’s name, the text field obtains the focus, and the soft keyboard is displayed. When you tap the OK button you notice that the text field still retains the focus, and the keyboard is still on screen.
Wouldn’t it be better, in terms of user experience, if the text field releases the focus, and, consequently, the keyboard is automatically hidden?
Given the code you added earlier, the boolean property:
@FocusState var nameFieldFocused: Bool
And the .focused() modifier applied to the text field:
TextField("Type your name...", text: $userManager.profile.name)
.focused($nameFieldFocused)
.submitLabel(.done)
.bordered()
If you want to release the focus, and automatically hide the keyboard, all you have to do is to set that property to false. The proper place to do that is in the button’s trigger handler (remember? It’s no longer called the tap handler :-)), which is the registerUser method.
So set that property to false at the beginning of registerUser():
func registerUser() {
// Add this line
nameFieldFocused = false
if userManager.settings.rememberUser {
userManager.persistProfile()
} else {
userManager.clear()
}
userManager.persistSettings()
userManager.setRegistered()
}
If you now run the app and tap the text field, when you tap the OK button the keyboard is automatically dismissed and the text field loses the focus. Mission accomplished!
One additional improvement is to make the keyboard’s Done button to replicate the tap on OK. You can easily do it by using, could you guess? That’s right, a modifier. Add this modifier to the text field:
TextField("Type your name...", text: $userManager.profile.name)
.focused($nameFieldFocused)
.submitLabel(.done)
// Add this modifier
.onSubmit(registerUser)
.bordered()
With the .onSubmit() modifier you’re asking the text field to execute registerMethod() when the submit button on the keyboard has been actioned.
Note that it also works if you press the Enter key on a physical keyboard (this is useful when running on macOS), but also in iOS — in case you don’t have a keyboard to connect to your phone, you can simply try in the simulator after connecting your mac keyboard (I/O -> Keyboard -> Connect Hardware Keyboard from the menu, or ⌘+⇧+K, to toggle on and off).
Note that .onSubmit() is not a modifier that works on and with the keyboard only. It comes into play when a control is submitted, whatever has been used for submitting it — tapping the Done button of the soft keyboard does cause a submit, and so does pressing the Enter key on a hardware keyboard, and similar equivalents for other platforms.
And, not least important, .onSubmit() is part of the View protocol — that means it is available to any view, although it has not much sense in some of them (think of a label, for example).
Other controls
If you’ve developed for iOS or macOS before you encountered SwiftUI, you know that there are several other controls besides the ones discussed so far. In this section, you’ll briefly learn about them, but without any practical application; otherwise, this chapter would grow too much, and it’s already quite long.
Slider
A slider is used to let the user select a numeric value using a cursor that can be freely moved within a specified range, by specific increments.
There are several initializers you can choose from, but probably the most used is:
public init<V>(
value: Binding<V>,
in bounds: ClosedRange<V>,
step: V.Stride = 1,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) where V : BinaryFloatingPoint, V.Stride : BinaryFloatingPoint
Which takes:
-
value: A value binding -
bounds: A range -
step: The interval of each step -
onEditingChanged: An optional closure called when editing starts or ends
Below is an example of this in action:
@State var amount: Double = 0
...
VStack {
HStack {
Text("0")
Slider(
value: $amount,
in: 0.0 ... 10.0,
step: 0.5
)
Text("10")
}
Text("\(amount)")
}
In this example, the slider is bound to the amount state property and is configured with an interval ranging from 0 to 10, and increments and decrements in steps of 0.5.
The HStack is used to add two labels at the left and right of the slider, specifying respectively the minimum and maximum values. The VStack is used to position a centered Text control below the slider, displaying the currently selected value.
Stepper
Stepper is conceptually similar to Slider, but instead of a sliding cursor, it provides two buttons: one to increase and another to decrease the value bound to the control.
There are several initializers, with one of the most common ones being this:
public init<S, V>(
_ title: S,
value: Binding<V>,
in bounds: ClosedRange<V>,
step: V.Stride = 1,
onEditingChanged: @escaping (Bool) -> Void = { _ in }
) where S : StringProtocol, V : Strideable
This takes the following arguments:
-
title: A title, usually containing the current bound value -
value: A value binding -
bounds: A range -
step: The interval of each step -
onEditingChanged: An optional closure called when editing starts or ends
An example of its usage is:
@State var quantity = 0.0
...
Stepper(
"Quantity: \(quantity)",
value: $quantity,
in: 0 ... 10,
step: 0.5
)
SecureField
SecureField is functionally equivalent to a TextField, differing by the fact that it hides the user input. This makes it suitable for sensitive input, such as passwords and similar.
It offers a few initializers, one of which is the following:
public init<S>(
_ title: S,
text: Binding<String>,
onCommit: @escaping () -> Void = {}
) where S : StringProtocol
Similar to the controls described earlier, it takes the following arguments:
-
title: A title, which is the placeholder text displayed inside the control when no input has been entered -
text: A text binding -
onCommit: An optional closure called when the user performs a commit action, such as pressing the Return key.
To use it for entering a password, you’d write something like:
@State var password = ""
...
SecureField.init("Password", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
Key points
Phew — what a long chapter. Congratulations for staying tuned and focused for so long! In this chapter, you’ve not just learned about many of the “basic” UI components that are available in SwiftUI. You’ve also learned the following facts:
- Refactoring and reusing views are two important aspects that should never be neglected or forgotten.
- You can create your own modifiers using
ViewModifier. - To handle user input, you use a
TextFieldcomponent or aSecureFieldif the input is sensitive. - Buttons are more flexible than their UIKit/AppKit counterparts and enable you to make any collection of views into a button.
- Validating input is much easier in SwiftUI, because you simply set the rules, and SwiftUI takes care of applying those rules when the state changes.
- SwiftUI has other controls to handle user input, like toggles, sliders, and steppers.
Where to go from here?
To learn more about controls in SwiftUI, you can check the following links:
- Official Documentation: Views and Controls apple.co/2MQgZG1
- WWDC 2019 - SwiftUI Essentials apple.co/2Le3qy6
In the next chapter, you’ll learn more about view containers. See you there!