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 6 — 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
Plus this one, which you must add to the iOS target only:
- iOS/Utils/KeyboardFollower.swift
Feel free to take a look at these files
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 6. 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?
So, open WelcomeView.swift, 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, create a new component view by right-clicking the Components group, and creating a new SwiftUI View named WelcomeBackgroundImage.swift - again, be sure to add to both targets, iOS and macOS. Then, paste in the body implementation 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.swift 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()
HStack {
...
Ensure that automatic preview is enabled (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.swift with the code you’ve copied from the welcome view.
If you open WelcomeView.swift and resume the preview, once again you won’t notice any differences — which means the refactoring worked.
Refactoring the welcome message
In WelcomeView.swift, 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.
-
Xcode is so kind as to put the new view name in edit mode, so you can type a new name in right away. 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.swift 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.swift 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.swift.
Next, replace its body implementation with:
VStack {
WelcomeMessageView()
}
And with a single line of code, you’ve just proved how easy and powerful 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.swift and replace WelcomeView with RegisterView:
var body: some Scene {
WindowGroup {
WelcomeView()
}
}
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.
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 can be used 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 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
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.swift, 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.
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 modifiers to buttons.
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 for a custom modifier. Next, change the protocol it 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.swift, 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.swift 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 use a struct named ModifiedContent, whose initializer takes two parameters:
- The content view
- The modifier
Open RegisterView, and embed the TextField in a ModifiedContent instantiation, 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.
A peek at TextField’s initializer
TextField has two pairs of initializers, 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 move the focus to the next field automatically.
The other 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.
Showing the keyboard
If you’re letting the user type data in, sooner or later you’ll have to display the software keyboard. Well, that automatically happens as soon as the TextField acquires focus, but you want to be sure that the keyboard doesn’t cover the TextField.
If you change the iOS Simulator to iPhone 8 or iPhone 11 and run the app, you’ll notice that when the keyboard is visible, it’s too close to the text field, although in this case it doesn’t actually overlap.
A very basic implementation of a keyboard handler is provided with the project, which you can find in Utils/KeyboardFollower.swift.
It uses Notification Center to subscribe for the keyboardWillChangeFrameNotification event, which stores a property keyboardHeight that contains the keyboard’s height. This is equal to zero if the keyboard is hidden, and a value greater than zero if it is visible.
So, you can subscribe for changes and use the keyboard’s height to alter the bottom padding of the view containing the TextField.
The first thing to do is add a new property directly after name, in RegisterView:
@ObservedObject var keyboardHandler: KeyboardFollower
The @ObservedObject attribute will be discussed in Chapter 8: “State & Data Flow — Part I”. For now, all you need to know is that it is similar to the @State attribute you encountered earlier, but in a different way, and, most importantly, applied to a custom class.
You need to initialize this property — and you can use dependency injection, which means you’re passing an instance of KeyboardFollower through the initializer.
Add the following to RegisterView below the keyboardHandler property:
init(keyboardHandler: KeyboardFollower) {
self.keyboardHandler = keyboardHandler
}
You need to pass this parameter in all places where RegisterView is instantiated. Scroll down to the preview provider, and change the previews property implementation to this:
RegisterView(keyboardHandler: KeyboardFollower())
Next, open KuchiApp.swift and do the same:
WindowGroup {
RegisterView(keyboardHandler: KeyboardFollower())
}
And, again, to the preview:
struct KuchiApp_Previews: PreviewProvider {
static var previews: some View {
RegisterView(keyboardHandler: KeyboardFollower())
}
}
Almost done! Lastly, back in RegisterView.swift, you need to add a bottom padding modifier to the VStack, using keyboardHandler.keyboardHeight for the length parameter. Add it before all other modifiers in the VStack:
.padding(.bottom, keyboardHandler.keyboardHeight)
Note: This is a new padding modifier that you have to add. You may notice that there is another padding, which you should not alter nor replace. It’s perfectly legit to have multiple padding modifiers, their effect is combined, and the result is the arithmetic sum of the padding applied to each direction.
With this line, you’re telling SwiftUI to apply dynamic padding, to the bottom of the containing view, that follows the following rules:
- When the keyboard is not visible,
keyboardHandler.keyboardHeightis zero, so no padding is applied. - When the keyboard is visible,
keyboardHandler.keyboardHeighthas a value greater than zero, so a padding equal to the keyboard height is applied.
That’s not all though. On phones with a safe area, the keyboard starts from the bottom edge of the screen up, so not including the safe area, whereas the padding specified above starts from the safe area.
To fix that, you can use the edgesIgnoringSafeArea modifier — Add this after the padding you added above:
.edgesIgnoringSafeArea(
keyboardHandler.isVisible ? .bottom : [])
Here you’re telling the view to ignore the bottom safe area, but only when the keyboard is visible.
For confirmation, the body implementation should look like this:
VStack(content: {
Spacer()
WelcomeMessageView()
TextField("Type your name...", text: $name)
.bordered()
Spacer()
})
.padding(.bottom, keyboardHandler.keyboardHeight)
.edgesIgnoringSafeArea(
keyboardHandler.isVisible ? .bottom : [])
.padding()
.background(WelcomeBackgroundImage())
If you run the app you should see the text field vertically centered (image at left), but when the text field has the focus, and the keyboard is displayed, the text field is moved toward the top (image at right).
Taps and buttons
Now that you’ve got a form, the most natural thing you’d want your user to do is to submit that 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, in macOS it can be a mouse click, in watchOS a digital crown press, and so forth.
Note: The button initializer takes the tap 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 trailing closure syntax. The reason is very likely because that pattern changes in SwiftUI, and the last parameter is always the view declaration — which, by the way, can use the same trailing closure syntax.
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: self.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, either in the Simulator or by activating the Live Preview, and when you press OK a message will be printed to the Xcode console. If you’ve chosen Live Preview, and nothing is displayed, be sure to enable Debug Preview from the menu accessible by right-clicking the Live Preview button.
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)
.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(keyboardHandler: KeyboardFollower())
.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, find and add these 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:
window.rootViewController = UIHostingController(
rootView: RegisterView(keyboardHandler: KeyboardFollower())
.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")
.resizable()
// 3
.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 centered, and with fixed 16×16 size.
- You change the label font, specifying a
.bodytype and a bold weight. - You apply the
.borderedmodifier, to add a blue border with rounded corners.
If you did everything correctly, this is what your preview should look like:
Reacting to input: validation
Now that you’ve concluded the whole keyboard affair, and 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.swift, 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.
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 TextView with the name you entered.
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. - When the keyboard is displayed, you must take care of avoiding overlapping the
TextField. For this, you can use the Notification Center and the keyboard’s height. - 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!