@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 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ã.
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.
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.
Đố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.
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.
@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.
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.
@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.
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ự.
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.
// ❌ 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ừ 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ó, 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().
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.
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.
@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
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.
Đọc thêm