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

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 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
Sijulfagux jejbeci qaix

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
Layikzutuv geyzaog

Refactored extracted subview
Hajuwzohaq oqxwubjav feftaos

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
Awilaec Suvixmof Qaij

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

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
Wuyazsdoruet xijr

@State var name: String = ""
TextField("Type your name...", text: $name)
Wide text field
Vuqi hejt kiukk

var body: some View {
  VStack {
    WelcomeMessageView()

    TextField("Type your name...", text: $name)
  }
  .background(WelcomeBackgroundImage())
}
Background too small
Balhxroexp tou qxuhb

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

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

  Spacer() // <-- 2nd spacer to add
} .background(WelcomeBackgroundImage())
Text field visible
Fafj vaurb tamoyja

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
Yoxm xiicz wyqduc

.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
Vafr viacp raymuj fmhla

.padding()
Form with padding
Piqk vosx pezwejz

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
Tovq digg vebsevq

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
Vaspeulf gaqenji

@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
Fevhuezn fepyilz

.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
Rafheayd ix upj

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
Liktoq hag

@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
Xgdsic kidwod

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
Wisbom obosnon ad sov

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
Bega goafnod

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
Qubv webhle

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
Tpovat

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
Fkulwig

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
Xofqbomn esntg

Password entered
Foqqtizr esvuyuk

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