Chapters

Hide chapters

SwiftUI by Tutorials

Second Edition · iOS 13 · Swift 5.2 · Xcode 11

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

Section II: Building Blocks of SwiftUI

Section 2: 6 chapters
Show chapters Hide chapters

7. Controls & User Input
Written by Antonio Bello

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In Chapter 6, you learned how to use two of the most commonly used controls: Text and Image. In this chapter, you’ll learn more about other commonly-used controls, such as TextField, Button, Stepper and more, as well as the power of refactoring.

A simple registration form

The Welcome to Kuchi screen you implemented in Chapter 6 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 the additional files needed in this chapter from the starter project:

  • Profile/Profile.swift
  • Profile/Settings.swift
  • Profile/UserManager.swift
  • Utils/KeyboardFollower.swift

It’s up to you if you prefer to create groups and folders for them or not.

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

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:

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

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

window.rootViewController = UIHostingController(
  rootView: WelcomeView()
)
window.rootViewController = UIHostingController(
  rootView: 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.

@State var name: String = ""
TextField("Type your name...", text: $name)

var body: some View {
  VStack {
    WelcomeMessageView()

    TextField("Type your name...", text: $name)
  }
  .background(WelcomeBackgroundImage())
}

VStack {
  Spacer() // <-- 1st spacer to add
  
  WelcomeMessageView()
  TextField("Type your name...", text: $name)
  
  Spacer() // <-- 2nd spacer to add
} .background(WelcomeBackgroundImage())

Styling the TextField

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

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

.padding()

Creating a custom text style

A custom text field style must adopt the TextFieldStyle, which declares one method only:

public func _body(
  configuration: TextField<Self._Label>) -> some View
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())

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.

@ObservedObject var keyboardHandler: KeyboardFollower
init(keyboardHandler: KeyboardFollower) {
  self.keyboardHandler = keyboardHandler
}
RegisterView(keyboardHandler: KeyboardFollower())
window.rootViewController = UIHostingController(
  rootView: RegisterView(keyboardHandler: KeyboardFollower())
)
.padding(.bottom, keyboardHandler.keyboardHeight)

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

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

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

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

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)

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

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

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
)

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

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 accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now