WWDC Quick Look 💓 By SwiftGGTeam
Direct and reflect focus in SwiftUI

Direct and reflect focus in SwiftUI

Watch original video

Highlight

iOS 15 introduces SwiftUI@FocusStateproperty wrappers andfocused()Modifiers that allow developers to programmatically control focus movement, automatically fall back to error fields when validating forms, and passfocusSection()Building large-scale focus navigation targets on tvOS.

Core Content

When making form-based apps, keyboard and focus management have always been pain points.

The previous approach was to mix UIKitbecomeFirstResponder()andresignFirstResponder(),passUIViewRepresentableBridging, cumbersome code and platform inconsistency.It’s even more painful on tvOS - the focus jump of the arrow keys is almost uncontrollable.

SwiftUI in iOS 15 uses a set of APIs to solve the focus management problem across the entire platform.

Programmatic control of focus

@FocusStateis a new property wrapper specifically designed to track and manage focus state.

enum Field: Hashable {
    case email
    case password
}

struct LoginView: View {
    @FocusState private var focusedField: Field?
    @State private var email = ""
    @State private var password = ""

    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)

            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
        }
    }
}

03:49

Key points:

  • focusedFieldIs an optional type, nil means there is no focus on the form area
  • FieldThe enumeration can be anyHashableType, string or integer can be used
  • RevisefocusedFieldvalue, the focus will automatically move to the corresponding view

Form validation and focus rollback

VStack {
    TextField("Email", text: $email)
        .focused($focusedField, equals: .email)
        .border(Color.red, width: (focusedField == .email && !isEmailValid) ? 2 : 0)

    SecureField("Password", text: $password)
        .focused($focusedField, equals: .password)
}
.onSubmit {
    submittedEmail = email
    if !isEmailValid {
        focusedField = .email
    } else {
        focusedField = nil // Validation passed; dismiss the keyboard
    }
}

06:07

When the user clicks the Go button on the keyboard,onSubmitis triggered.If the email format is incorrect, the focus will automatically return to the email input box and a red border prompt will be displayed.After passing the verification, setfocusedField = nil, the keyboard automatically retracts.

Large focus navigation on tvOS

tvOS’s focus navigation is based on proximity relationships.If two focusable views are not adjacent, the arrow keys cannot jump between them.

focusSection()Modifiers can mark a container view as the focus area, allowing focus to “jump” to non-adjacent views.

HStack {
    VStack {
        TextField("Email", text: $email)
        SecureField("Password", text: $password)
    }
    .focusSection() // Left focus area

    VStack {
        Image(photoName)
        Button("Browse Photos") {}
    }
    .focusSection() // Right focus area
}

09:47

addfocusSection()Finally, if the user presses the right button of the remote control in the left input box, the focus will jump directly to the Browse Photos button on the right, even if they are far apart on the screen.

Detailed Content

Complete login form example

import SwiftUI
import AuthenticationServices

enum Field: Hashable {
    case email
    case password
}

struct ContentView: View {
    @FocusState private var focusedField: Field?
    @State private var email: String = ""
    @State private var password: String = ""
    @State private var submittedEmail: String = ""

    var body: some View {
        VStack(alignment: .center) {
            Text("Vacation Planner")
                .font(.custom("Baskerville-SemiBoldItalic", size: 60))

            TextField("Email", text: $email)
                .submitLabel(.next)
                .textContentType(.emailAddress)
                .keyboardType(.emailAddress)
                .padding()
                .frame(height: 50)
                .background(Color.white.opacity(0.9))
                .cornerRadius(15)
                .padding(10)
                .focused($focusedField, equals: .email)
                .border(Color.red,
                        width: (focusedField == .email && !isEmailValid) ? 2 : 0)

            SecureField("Password", text: $password)
                .submitLabel(.go)
                .textContentType(.password)
                .padding()
                .frame(height: 50)
                .background(Color.white.opacity(0.9))
                .cornerRadius(15)
                .padding(10)
                .focused($focusedField, equals: .password)

            SignInWithAppleButton(.signIn) { request in
                request.requestedScopes = [.fullName, .email]
            } onCompletion: { result in
                switch result {
                case .success:
                    print("Authorization successful.")
                case .failure(let error):
                    print("Authorization failed: " + error.localizedDescription)
                }
            }
            .frame(height: 50)
            .cornerRadius(15)
        }
        .onSubmit {
            submittedEmail = email
            if !isEmailValid {
                focusedField = .email
            } else {
                focusedField = nil
            }
        }
    }

    private var isEmailValid: Bool {
        let regex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
        let predicate = NSPredicate(format: "SELF MATCHES %@", regex)
        return submittedEmail.isEmpty || predicate.evaluate(with: submittedEmail)
    }
}

06:25

Key points:

  • .submitLabel(.next)and.submitLabel(.go)Control the button text in the lower right corner of the keyboard
  • .textContentType(.emailAddress)Let the system provide autocomplete suggestions
  • .keyboardType(.emailAddress)Pops up a keyboard layout suitable for entering email addresses
  • .border()The conditional expression dynamically displays a red border based on the focus state and validation results.

Cross-platform focus management

@FocusStateNot limited to text input boxes.Any focusable view works, including iOS, tvOS, watchOS, and macOS.

struct SettingsView: View {
    @FocusState private var focusedButton: String?

    var body: some View {
        VStack {
            Button("Notifications") {}
                .focused($focusedButton, equals: "notifications")

            Button("Privacy") {}
                .focused($focusedButton, equals: "privacy")

            Button("About") {}
                .focused($focusedButton, equals: "about")
        }
    }
}

Core Takeaways

1. Add smart navigation to complex forms

During the registration process, after the user fills in a field and presses Enter, the focus automatically moves to the next required field.If validation of the current field fails, the focus remains in place and an error message is displayed.Entrance API:@FocusState + .submitLabel() + .onSubmit()

2. Clear the search box with one click

Add a clear button to the search interface. After clicking it, the search text will be cleared and the search text will be automatically refocused to the search box. Users can enter new keywords directly.Entrance API:focusedField = .search

3. Design sidebar + content layout for tvOS applications

usefocusSection()Mark the sidebar and content area as two independent focus areas. Users can use the remote control direction keys to jump freely between areas without traversing all the elements in the middle one by one.Entrance API:.focusSection()

4. Build a multi-step wizard interface

In the setup wizard, after each step is completed, the focus automatically moves to the first input box of the next step.use@FocusStateEnumerations represent the combined state of steps and fields.Entrance API:@FocusState + withAnimation

5. Add shortcut key navigation to macOS applications

combinekeyboardShortcut()and@FocusState, letting the user press Tab to cycle through form fields and Cmd+Enter to submit.Entrance API:.keyboardShortcut(.tab) + @FocusState

Comments

GitHub Issues · utterances