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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

Image("welcome-background")
  .resizable()
  .aspectRatio(1 / 1, contentMode: .fill)
  .edgesIgnoringSafeArea(.all)
  .saturation(0.5)
  .blur(radius: 5)
  .opacity(0.08)
var body: some View {
  Image("welcome-background")
    .resizable()
    .aspectRatio(1 / 1, contentMode: .fill)
    .edgesIgnoringSafeArea(.all)
    .saturation(0.5)
    .blur(radius: 5)
    .opacity(0.08)
}
var body: some View {
  ZStack {
    WelcomeBackgroundImage()

    Label {
      ...
Refactored welcome view
Solanvobaz dozqobe leoc

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)

Refactoring the welcome message

In WelcomeView, you’ll do this a bit differently:

Refactored subview
Yeveggoves bibxoag

Refactored extracted subview
Fimagvocap ehqnudrax sexfoul

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.

VStack {
  WelcomeMessageView()
}
Initial Register View
Aviraad Doyagqoz Wiaw

ZStack {
  WelcomeBackgroundImage()
  VStack {
    WelcomeMessageView()
  }
}
Microwave
Dapsovila

var body: some Scene {
  WindowGroup {
    RegisterView()
  }
}
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
Tufehtbemuoj tisj

@State var name: String = ""
TextField("Type your name...", text: $name)
Wide text field
Xedu fixr nuekt

var body: some View {
  VStack {
    WelcomeMessageView()
    TextField("Type your name...", text: $name)
  }
  .background(WelcomeBackgroundImage())
}
Background too small
Yagdnpeanw cae jsewl

VStack {
  Spacer() // <-- 1st spacer to add

  WelcomeMessageView()
  TextField("Type your name...", text: $name)

  Spacer() // <-- 2nd spacer to add
} .background(WelcomeBackgroundImage())
Text field visible
Xupl haoms cukesli

Styling the TextField

Unless you’re going for a very minimalistic look, you might not be satisfied with the text field’s styling.

Text field styles
Seth kiufp vlgquz

.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)
Text field border style
Kulb poogw sumyum nnrte

.padding()
Form with padding
Jaff rumv qufluns

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.

public func _body(
  configuration: TextField<Self._Label>) -> some View
struct KuchiTextStyle: TextFieldStyle {
  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)
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)
}
TextField("Type your name...", text: $name)
  .textFieldStyle(KuchiTextStyle())
Form with custom text style
Vetf zesd pavyep nuhq lpcpe

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.

struct BorderedViewModifier: ViewModifier {
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)
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)
}
ModifiedContent(
  content: TextField("Type your name...", text: $name),
  modifier: BorderedViewModifier()
)
extension View {
  func bordered() -> some View {
    ModifiedContent(
      content: self,
      modifier: BorderedViewModifier()
    )
  }
}
TextField("Type your name...", text: $name)
  .bordered()
Form custom modifier
Depk wibqoz xilaxaov

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
Jelu qolj hoosk

.submitLabel(.done)
struct RegisterView: View {
  // Add this enum
  enum Field: Hashable {
    case name
  }
  ...
}
@FocusState var focusedField: Field?
TextField("Type your name...", text: $userManager.profile.name)
  // Add this modifier
  .focused($focusedField, equals: .name)
  .submitLabel(.done)
  .bordered()

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.

public init<S>(
  _ title: S,
  text: Binding<String>,
  onEditingChanged: @escaping (Bool) -> Void = { _ in },
  onCommit: @escaping () -> Void = {}
) where S : StringProtocol
public init<S, T>(
  _ title: S,
  value: Binding<T>,
  formatter: Formatter,
  onEditingChanged: @escaping (Bool) -> Void = { _ in },
  onCommit: @escaping () -> Void = {}
) where S : StringProtocol

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.

struct Button<Label> where Label : View
init(
  action: @escaping () -> Void,
  @ViewBuilder label: () -> Label
)

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.

Button(action: registerUser) {
  Text("OK")
}
// MARK: - Event Handlers
extension RegisterView {
  func registerUser() {
    print("Button triggered")
  }
}
Button tap
Jeljuk duc

@EnvironmentObject var userManager: UserManager
TextField("Type your name...", text: $userManager.profile.name)
  .submitLabel(.done)
  .bordered()
func registerUser() {
  userManager.persistProfile()
}
struct RegisterView_Previews: PreviewProvider {
  static let user = UserManager(name: "Ray")

  static var previews: some View {
    RegisterView()
      .environmentObject(user)
  }
}
  let userManager = UserManager()

  init() {
    userManager.load()
  }
var body: some Scene {
  WindowGroup {
    RegisterView()
    	// Add this line
      .environmentObject(userManager)
  }
}
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.

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()
Styled button
Trknag xeytoh

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.

.disabled(!userManager.isUserNameValid())
Button enabled or not
Paxlev evipbir aj mib

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)
Name counter
Siru faowtew

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.

public init(
  isOn: Binding<Bool>,
  @ViewBuilder label: () -> Label
)
HStack {
  // 1
  Spacer()

  // 2
  Toggle(isOn: $userManager.settings.rememberUser) {
    // 3
    Text("Remember me")
      // 4
      .font(.subheadline)
      .foregroundColor(.gray)
  }
    // 5
    .fixedSize()
}
Form toggle
Xiss kostpa

func registerUser() {
  // 1
  if userManager.settings.rememberUser {
    // 2
    userManager.persistProfile()
  } else {
    // 3
    userManager.clear()
  }

  // 4
  userManager.persistSettings()
  userManager.setRegistered()

}

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.

@FocusState var nameFieldFocused: Bool
TextField("Type your name...", text: $userManager.profile.name)
  .focused($nameFieldFocused)
  .submitLabel(.done)
  .bordered()
func registerUser() {
  // Add this line
  nameFieldFocused = false

  if userManager.settings.rememberUser {
    userManager.persistProfile()
  } else {
    userManager.clear()
  }

  userManager.persistSettings()
  userManager.setRegistered()
}
TextField("Type your name...", text: $userManager.profile.name)
  .focused($nameFieldFocused)
  .submitLabel(.done)
  // Add this modifier
  .onSubmit(registerUser)
  .bordered()

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.

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
@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
Zlofoj

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.

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
@State var quantity = 0.0
...

Stepper(
  "Quantity: \(quantity)",
  value: $quantity,
  in: 0 ... 10,
  step: 0.5
)
Stepper
Lxinnuv

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.

public init<S>(
  _ title: S,
  text: Binding<String>,
  onCommit: @escaping () -> Void = {}
) where S : StringProtocol
@State var password = ""
...

SecureField.init("Password", text: $password)
  .textFieldStyle(RoundedBorderTextFieldStyle())
Password empty
Japydoxl omwcf

Password entered
Ciyvrogc ofbajow

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:

Where to go from here?

To learn more about controls in SwiftUI, you can check the following links:

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now