Chapters

Hide chapters

SwiftUI by Tutorials

Third Edition · iOS 14 · Swift 5.3 · Xcode 12

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

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.

Refactored welcome view
Refactored welcome view

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 LogoImage and WelcomeMessageView.

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 body implementation 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:

Refactored subview
Refactored subview

  • Choose Extract Subview. Xcode will replace the selected component with ExtractedView(), and will move its implementation at the end of the file, in a new ExtractedView struct.

Refactored extracted subview
Refactored extracted subview

  • 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 WelcomeMessageView and press Enter.

  • Now you’re going to move it to a new file. Select the entire WelcomeMessageView struct and cut it.

  • Next, create a new WelcomeMessageView.swift file in the Components group, using the SwiftUI template.

  • Replace the implementation of WelcomeMessageView with 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.

Initial Register View
Initial Register View

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!

Microwave
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.

Registration form
Registration form

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 @State attribute 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:

Wide text field
Wide text field

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 backgroundColor property, which can be used to specify a uniform background color. The SwiftUI counterpart is more polymorphic; the .background modifier accepts any type that conforms to View, which includes Color, Image, Shape, among others.

With this change, the text field is now visible, but the background looks too small.

Background too small
Background 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.

Text field visible
Text field visible

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:

Text field styles
Text field styles

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 TextField as needed.
  • Create your own text field style, by defining a concrete type conforming to the TextFieldStyle protocol.
  • Create a custom modifier, by defining a concrete type conforming to the ViewModifier protocol.

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:

Rext field border style
Rext field border style

This is what each does:

  1. Creates an unmodified text field.
  2. Adds padding of 16 points vertically, and 8 points horizontally.
  3. Adds a non-transparent white background.
  4. Creates an overlay for the border, using a rounded rectangle with a corner radius of 8.
  5. Adds a stroke effect to keep the border only, leaving the content behind visible.
  6. Makes the border blue.
  7. 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()

Form with padding
Form with 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.

Form with padding
Form with padding

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 is true) or loses focus (when the parameter is false).

  • 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:

  1. The formatter parameter, which is an instance of a class inherited from Foundation’s abstract class Formatter. 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.

  2. The T generic 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.

Keyboard visible
Keyboard visible

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.keyboardHeight is zero, so no padding is applied.
  • When the keyboard is visible, keyboardHandler.keyboardHeight has a value greater than zero, so a padding equal to the keyboard height is applied.

Keyboard padding
Keyboard padding

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).

Keyboard on off
Keyboard on off

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.

Button tap
Button tap

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:

  1. As previously stated, the label parameter 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.
  2. You add a checkmark icon.
  3. You make the icon centered, and with fixed 16×16 size.
  4. You change the label font, specifying a .body type and a bold weight.
  5. You apply the .bordered modifier, to add a blue border with rounded corners.

If you did everything correctly, this is what your preview should look like:

Styled button
Styled button

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.

Button enabled or not
Button enabled or not

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:

  1. You use a spacer to push the Text to the right, in a pseudo-right-alignment way.
  2. This is a simple Text control, whose text is the count of characters of the name property.
  3. You use a green text color if the input passes validation, red otherwise.
  4. 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.

Name counter
Name counter

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()
}

Form toggle
Form toggle

The code is very simple and straightforward:

  1. You need the spacer to add flexible spacing to the left, to push the toggle toward the right, and make it right-aligned.
  2. You create the Toggle component, binding to $userManager.settings.rememberUser.
  3. This is the label displayed before the component itself.
  4. You alter the default style of the label to make it smaller and gray.
  5. 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:

  1. You check if the user chose whether to remember herself or not.
  2. If yes, then make the profile persistent.
  3. Otherwise, clear the user defaults.
  4. 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:

  1. value: A value binding
  2. bounds: A range
  3. step: The interval of each step
  4. 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)")
}

Slider
Slider

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:

  1. title: A title, usually containing the current bound value
  2. value: A value binding
  3. bounds: A range
  4. step: The interval of each step
  5. 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
)

Stepper
Stepper

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:

  1. title: A title, which is the placeholder text displayed inside the control when no input has been entered
  2. text: A text binding
  3. 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())

Password empty
Password empty

Password entered
Password entered

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 TextField component or a SecureField if 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:

In the next chapter, you’ll learn more about view containers. See you there!

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.