@FocusState — nó là gì, quản lý tiêu điểm và bàn phím trong SwiftUI

Tác giả: IT Sectr Đã đăng: 2026-06-26 Thời gian đọc: 9 phút

@FocusState là một property wrapper trong SwiftUI, được giới thiệu trong iOS 15, cho phép bạn điều khiển tiêu điểm nhập liệu trên các trường văn bản và các phần tử khác một cách lập trình. Trước khi nó xuất hiện, các nhà phát triển phải sử dụng UIViewRepresentable để truy cập các phương thức UIKit becomeFirstResponder và resignFirstResponder. @FocusState giải quyết vấn đề này một cách tự nhiên: bạn liên kết một thuộc tính với một trường thông qua bổ cục .focused(), sau đó việc đặt hoặc xóa tiêu điểm được thực hiện bằng một phép gán giá trị đơn giản. Theo Apple Developer Documentation — FocusState (2025), @FocusState hỗ trợ hai chế độ: Bool để quản lý đơn giản (tiêu điểm bật hoặc tắt) và enum cho nhiều trường, trong đó mỗi case tương ứng với một trường nhập liệu cụ thể.

Những điểm chính

  • @FocusState — một property wrapper để quản lý tiêu điểm lập trình trong SwiftUI, có sẵn từ iOS 15.
  • Chế độ Bool — cho một trường duy nhất, sử dụng @FocusState var isFocused: Bool với .focused($isFocused).
  • Chế độ Enum — cho nhiều trường, sử dụng enum tuân thủ giao thức FocusStateValue và .focused($field, equals: .fieldName).
  • Ẩn bàn phím — đặt tiêu điểm thành nil hoặc false để ẩn bàn phím.
  • Tiêu điểm tự động — đặt giá trị ban đầu trong .onAppear để hiển thị bàn phím khi mở màn hình.

@FocusState trong SwiftUI là gì

@FocusState là một property wrapper liên kết trạng thái tiêu điểm với một trường nhập liệu cụ thể hoặc phần tử có thể tập trung khác trong SwiftUI. Không giống như UIKit, nơi quản lý tiêu điểm diễn ra thông qua các phương thức becomeFirstResponder và resignFirstResponder, SwiftUI sử dụng cách tiếp cận khai báo: bạn khai báo một trạng thái (@FocusState) và liên kết nó với một phần tử thông qua bổ cục .focused(). Thay đổi trạng thái sẽ tự động thay đổi tiêu điểm.

Trước khi @FocusState được giới thiệu trong iOS 15, các nhà phát triển phải tạo các wrapper UIViewRepresentable xung quanh UITextField hoặc sử dụng thư viện bên thứ ba. @FocusState được tích hợp trực tiếp vào SwiftUI và hoạt động với TextField, TextEditor, SecureField và SearchField. Điều này làm cho mã sạch hơn, giảm số lượng cầu nối UIKit và cải thiện khả năng kiểm thử.

Theo WWDC Session 10136 — What's new in SwiftUI (2024), @FocusState sử dụng hệ thống preference key của SwiftUI để truyền thông tin tiêu điểm giữa các phần tử. Khi một trường nhận được tiêu điểm, SwiftUI tự động cập nhật thuộc tính @FocusState được liên kết, cho phép bạn phản ứng với các thay đổi tiêu điểm trong mã.

Quản lý tiêu điểm với Bool

Cách đơn giản nhất để sử dụng @FocusState là với kiểu Bool. Khi một trường được tiêu điểm, thuộc tính là true. Khi mất tiêu điểm — false. Bạn có thể buộc tiêu điểm bằng cách đặt nó thành true hoặc xóa nó bằng cách đặt thành false.

swift
struct LoginForm: View {
    @State var email = ""
    @FocusState var isEmailFocused: Bool
    
    var body: some View {
        VStack {
            TextField("Email", text: $email)
                .focused($isEmailFocused)
            
            Button("Hiển thị bàn phím") {
                isEmailFocused = true
            }
            Button("Ẩn bàn phím") {
                isEmailFocused = false
            }
        }
    }
}

Trong ví dụ này, isEmailFocused tự động trở thành true khi người dùng chạm vào trường văn bản và false khi bàn phím được ẩn. Các nút cho phép bạn quản lý tiêu điểm theo cách lập trình — hữu ích cho bàn phím tùy chỉnh, nút "Tiếp theo" và các tình huống bạn cần buộc ẩn bàn phím sau khi gửi biểu mẫu.

Quản lý tiêu điểm với Enum cho nhiều trường

Đối với biểu mẫu có nhiều trường, @FocusState hỗ trợ enum tuân thủ giao thức FocusStateValue (hoặc Hashable). Mỗi case enum tương ứng với một trường cụ thể. Điều này cho phép chuyển đổi tiêu điểm giữa các trường — ví dụ: khi người dùng nhấn "Tiếp theo" trên bàn phím để di chuyển đến trường tiếp theo.

swift
struct RegistrationForm: View {
    enum Field: Hashable {
        case email
        case password
        case confirmPassword
    }
    
    @State var email = ""
    @State var password = ""
    @State var confirmPassword = ""
    @FocusState var focusedField: Field?
    
    var body: some View {
        Form {
            TextField("Email", text: $email)
                .focused($focusedField, equals: .email)
                .onSubmit { focusedField = .password }
            
            SecureField("Password", text: $password)
                .focused($focusedField, equals: .password)
                .onSubmit { focusedField = .confirmPassword }
            
            SecureField("Confirm", text: $confirmPassword)
                .focused($focusedField, equals: .confirmPassword)
                .onSubmit { submitForm() }
        }
    }
}

Hãy chú ý bổ cục .onSubmit — nó được gọi khi người dùng nhấn "Return" trên bàn phím. Bên trong .onSubmit, chúng tôi chuyển focusedField sang trường tiếp theo, tự động di chuyển tiêu điểm. Trường cuối cùng gọi submitForm() để gửi biểu mẫu.

Ẩn và hiện bàn phím

@FocusState cung cấp một cách đơn giản để ẩn bàn phím — chỉ cần đặt thuộc tính thành nil (cho enum) hoặc false (cho Bool). Tuy nhiên, đôi khi bạn cần ẩn bàn phím mà không liên kết nó với một trường cụ thể — ví dụ: khi chạm vào khoảng trống. Trong trường hợp này, có một số cách tiếp cận.

swift
struct DismissKeyboardView: View {
    @State var text = ""
    @FocusState var isFocused: Bool
    
    var body: some View {
        TextField("Enter text", text: $text)
            .focused($isFocused)
            .toolbar {
                ToolbarItemGroup(placement: .keyboard) {
                    Spacer()
                    Button("Xong") {
                        isFocused = false
                    }
                }
            }
    }
}

Bổ cục .toolbar với placement .keyboard thêm một nút phía trên bàn phím. Đây là một mẫu UX tiêu chuẩn trong iOS để ẩn bàn phím. Một cách tiếp cận thay thế là sử dụng .onTapGesture trên VStack gốc để đặt lại tiêu điểm khi chạm vào nền.

Tiêu điểm và xác thực biểu mẫu

@FocusState kết hợp hoàn hảo với xác thực biểu mẫu. Một mẫu điển hình: sau khi nhấn nút "Gửi", xác thực tất cả các trường và đặt tiêu điểm vào trường đầu tiên có lỗi. Điều này cải thiện trải nghiệm người dùng — người dùng ngay lập tức thấy trường nào cần được sửa.

swift
struct ValidatedForm: View {
    enum Field: Hashable { case name; case phone }
    
    @State var name = ""
    @State var phone = ""
    @FocusState var focusedField: Field?
    @State var errors: [String] = []
    
    var body: some View {
        Form {
            TextField("Name", text: $name)
                .focused($focusedField, equals: .name)
            TextField("Phone", text: $phone)
                .focused($focusedField, equals: .phone)
            
            Button("Gửi") { validateAndSubmit() }
        }
    }
    
    func validateAndSubmit() {
        if name.isEmpty {
            focusedField = .name
            return
        }
        if phone.isEmpty {
            focusedField = .phone
            return
        }
        // gửi biểu mẫu
    }
}

Trong ví dụ này, nếu trường name trống, tiêu điểm sẽ di chuyển đến nó và người dùng ngay lập tức thấy lỗi ở đâu. Nếu name đã được điền, phone sẽ được kiểm tra. Đây là hành vi tự nhiên cho biểu mẫu — người dùng điền các trường từ trên xuống dưới và xác thực theo cùng thứ tự.

Các lỗi thường gặp với @FocusState

Lỗi phổ biến nhất là cố gắng sử dụng @FocusState với một kiểu không tuân thủ Hashable. @FocusState yêu cầu kiểu thuộc tính phải là Hashable (Bool và enum tùy chọn đã tuân thủ). Nếu bạn đang cố gắng sử dụng một cấu trúc tùy chỉnh, hãy đảm bảo nó triển khai Hashable.

  • Quên bổ cục .focused() — @FocusState tự nó không quản lý tiêu điểm. Bạn phải liên kết nó với một trường thông qua .focused($property) hoặc .focused($property, equals: .case).
  • Nhiều @FocusState trong một View — cho nhiều trường, hãy sử dụng một @FocusState duy nhất với enum, không phải nhiều thuộc tính @FocusState. Nhiều thuộc tính Bool sẽ không được đồng bộ hóa với nhau.
  • Thay đổi @FocusState ngoài luồng chính — @FocusState chỉ nên được thay đổi trên luồng chính, giống như tất cả các thuộc tính UI trong SwiftUI. Các hoạt động bất đồng bộ phải chuyển sang MainActor trước khi thay đổi nó.
  • Đặt lại tiêu điểm khi xây dựng lại View — nếu View được xây dựng lại, @FocusState có thể bị đặt lại. Sử dụng bổ cục .id() để nhận dạng View ổn định.
swift
// ❌ Sai: hai @FocusState Bool thay vì enum
@FocusState var isNameFocused: Bool
@FocusState var isEmailFocused: Bool

// ✅ Đúng: một enum @FocusState duy nhất
enum Field: Hashable { case name; case email }
@FocusState var focusedField: Field?

Các câu hỏi thường gặp

@FocusState có sẵn từ những phiên bản iOS nào?

@FocusState có sẵn từ iOS 15, iPadOS 15, macOS 12, tvOS 15 và watchOS 8. Đối với các dự án hỗ trợ iOS 14 trở xuống, hãy sử dụng UIViewRepresentable với UITextField và becomeFirstResponder, hoặc các thư viện bên thứ ba có triển khai quản lý tiêu điểm tùy chỉnh.

Có thể sử dụng @FocusState với UIViewRepresentable tùy chỉnh không?

Có, bạn cần triển khai hỗ trợ FocusState trong UIViewRepresentable tùy chỉnh thông qua giao thức UIViewRepresentable. View tùy chỉnh phải có becomeFirstResponder và resignFirstResponder. SwiftUI sẽ tự động liên kết @FocusState với các phương thức này nếu bạn chỉ định bổ cục .focused().

Tại sao @FocusState không hoạt động với TextField trong List?

Trong List hoặc Form, các ô có thể được tái sử dụng, điều này phá vỡ liên kết @FocusState với trường. Giải pháp: thêm bổ cục .id() với một định danh duy nhất cho mỗi TextField. Ví dụ: .id(fieldName). Điều này buộc SwiftUI tạo một phiên bản View riêng cho mỗi trường.

Làm thế nào để ẩn bàn phím khi chạm vào khoảng trống?

Thêm .onTapGesture vào container gốc (VStack, ZStack) và đặt lại tiêu điểm: focusedField = nil. Tuy nhiên, .onTapGesture có thể chặn các chạm vào nút bên trong — hãy sử dụng container với .contentShape(Rectangle()) và .onTapGesture trên đó, hoặc UIKitBackgroundView tùy chỉnh.

Làm thế nào để tạo hiệu ứng xuất hiện bàn phím với @FocusState?

@FocusState không cung cấp API trực tiếp cho hiệu ứng bàn phím — đây là hành vi hệ thống iOS. Tuy nhiên, bạn có thể phản ứng với các thay đổi tiêu điểm bằng .onChange(of: focusedField) hoặc .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) để tạo hiệu ứng nội dung tùy chỉnh.

Tổng kết

  • @FocusState — một property wrapper SwiftUI gốc để quản lý tiêu điểm nhập liệu, có sẵn từ iOS 15.
  • Hai chế độ — Bool cho một trường duy nhất, enum Hashable cho nhiều trường biểu mẫu.
  • Bổ cục .focused() — bắt buộc để liên kết @FocusState với một trường nhập liệu cụ thể.
  • Điều khiển lập trình — đặt giá trị thành nil hoặc false sẽ ẩn bàn phím.
  • Xác thực biểu mẫu — @FocusState cho phép đặt tiêu điểm vào trường đầu tiên có lỗi sau khi xác thực.
  • Enum cho nhiều trường — một @FocusState duy nhất với enum được ưu tiên hơn nhiều thuộc tính Bool.
  • iOS 15+ — cho các phiên bản cũ hơn, hãy sử dụng UIViewRepresentable với becomeFirstResponder.

Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay

IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.

Thảo luận dự án

Đọc thêm