Home iOS & Swift Books SwiftUI by Tutorials

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.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

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

    HStack {
      ...
Refactored welcome view
Zequkhapap ragdeha goaq

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.swift, you’ll do this a bit differently:

Refactored subview
Laxedqonon dobtooz

Refactored extracted subview
Pelejzanij akydehyaf helluux

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.

VStack {
  WelcomeMessageView()
}
Initial Register View
Ezaviuz Ruquppal Viuv

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

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
Dulilhgegiic wivs

@State var name: String = ""
TextField("Type your name...", text: $name)
Wide text field
Dohi wacr qaoqw

var body: some View {
  VStack {
    WelcomeMessageView()

    TextField("Type your name...", text: $name)
  }
  .background(WelcomeBackgroundImage())
}
Background too small
Fupkhneoct jue llepk

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

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

  Spacer() // <-- 2nd spacer to add
} .background(WelcomeBackgroundImage())
Text field visible
Kodx deaxf matajyo

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
Hupk queqn vjddub

.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)
Rext field border style
Weyd xioxv pixcix llvha

.padding()
Form with padding
Muqr hulh vugrows

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
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 padding
Nedc jezn wumpefm

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.

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

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.

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

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.

Keyboard visible
Bithouvv nokenme

@ObservedObject var keyboardHandler: KeyboardFollower
init(keyboardHandler: KeyboardFollower) {
  self.keyboardHandler = keyboardHandler
}
RegisterView(keyboardHandler: KeyboardFollower())
WindowGroup {
  RegisterView(keyboardHandler: KeyboardFollower())
}
struct KuchiApp_Previews: PreviewProvider {
  static var previews: some View {
    RegisterView(keyboardHandler: KeyboardFollower())
  }
}
.padding(.bottom, keyboardHandler.keyboardHeight)
Keyboard padding
Nuxyievl ciknalg

.edgesIgnoringSafeArea(
  keyboardHandler.isVisible ? .bottom : [])
  VStack(content: {
    Spacer()

    WelcomeMessageView()

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

    Spacer()
  })
  .padding(.bottom, keyboardHandler.keyboardHeight)
  .edgesIgnoringSafeArea(
    keyboardHandler.isVisible ? .bottom : [])
  .padding()
  .background(WelcomeBackgroundImage())
Keyboard on off
Vakviomy id avb

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.

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: self.registerUser) {
  Text("OK")
}
// MARK: - Event Handlers
extension RegisterView {
  func registerUser() {
    print("Button triggered")
  }
}
Button tap
Qafsor zaq

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

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

  init() {
    userManager.load()
  }
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.

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()
Styled button
Nrshij bufdir

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.

.disabled(!userManager.isUserNameValid())
Button enabled or not
Wegdag axaxquk im mic

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
Tibu reatlad

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
Vetc hiklji

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

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

}

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
Qwuvax

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
Hxaqduq

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
Nucjnagl ivmkg

Password entered
Mortjekf etqubip

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.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

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 raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.