Chapters

Hide chapters

SwiftUI by Tutorials

Fourth Edition · iOS 15, macOS 12 · Swift 5.5 · Xcode 13.1

Before You Begin

Section 0: 4 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 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.

Adding the Profile group
Adding the Profile group

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.

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

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

  • If Xcode is not so kind to put the new view name in edit mode, right click on ExtractedView and choose Refactor and then Rename.
  • Type a new name in — 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 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 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.

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

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.

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:

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 you can use 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 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:

Text field border style
Text 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

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.

Form with custom text style
Form with custom text style

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.

Form custom modifier
Form custom modifier

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.

Wide text field
Wide text field

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 focusedField property 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 focusField is .name, give this text field focus.
  • When this field gets focus, set focusField to .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 Field enum altogether.

  • Next, replace the focusField property with this new implementation:

    @FocusState var nameFieldFocused: Bool
    

    The new property is a boolean, so it can be either true or false, 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 .focused modifier 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 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 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:

  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.

New to SwiftUI 3, there’s also a new pair of parameters that you can pass to the initializer:

  • prompt lets you pass a Text instance that will be used for the placeholder text — The difference compared to the title parameter is that you can apply custom formatting, such as changing font.
  • label lets you pass a View which 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.

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

  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 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.
  4. You change the label font, specifying a .body type and a bold weight.
  5. You apply the .bordered modifier 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:

Styled button
Styled button

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.

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

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