@FocusState คือ property wrapper ใน SwiftUI ที่ถูกนำเสนอใน iOS 15 ซึ่งช่วยให้คุณสามารถควบคุมโฟกัสการป้อนข้อมูลบนช่องข้อความและองค์ประกอบอื่นๆ ได้โดยทางโปรแกรม ก่อนที่จะมีฟีเจอร์นี้ นักพัฒนาต้องใช้ UIViewRepresentable เพื่อเข้าถึงเมธอด UIKit อย่าง becomeFirstResponder และ resignFirstResponder @FocusState แก้ปัญหานี้ได้อย่างเป็นธรรมชาติ: คุณผูกคุณสมบัติกับช่องผ่านตัวปรับแต่ง .focused() หลังจากนั้นการตั้งค่าหรือลบโฟกัสจะทำได้โดยการกำหนดค่าแบบง่าย ตาม Apple Developer Documentation — FocusState (2025) @FocusState รองรับสองโหมด: Bool สำหรับการจัดการแบบง่าย (โฟกัสเปิดหรือปิด) และ enum สำหรับหลายช่อง โดยที่แต่ละ case สอดคล้องกับช่องป้อนข้อมูลที่เฉพาะเจาะจง
ประเด็นสำคัญ
@FocusState คือ property wrapper ที่เชื่อมโยงสถานะโฟกัสกับช่องป้อนข้อมูลที่เฉพาะเจาะจงหรือองค์ประกอบที่สามารถโฟกัสได้อื่นๆ ใน SwiftUI แตกต่างจาก UIKit ที่การจัดการโฟกัสเกิดขึ้นผ่านเมธอด becomeFirstResponder และ resignFirstResponder SwiftUI ใช้แนวทางแบบประกาศ: คุณประกาศสถานะ (@FocusState) และเชื่อมโยงกับองค์ประกอบผ่านตัวปรับแต่ง .focused() การเปลี่ยนสถานะจะเปลี่ยนโฟกัสโดยอัตโนมัติ
ก่อนที่ @FocusState จะถูกนำเสนอใน iOS 15 นักพัฒนาต้องสร้าง wrapper UIViewRepresentable รอบ UITextField หรือใช้ไลบรารีของบุคคลที่สาม @FocusState ถูกผสานรวมโดยตรงใน SwiftUI และทำงานกับ TextField, TextEditor, SecureField และ SearchField ทำให้โค้ดสะอาดขึ้น ลดจำนวนสะพาน UIKit และปรับปรุงความสามารถในการทดสอบ
ตาม WWDC Session 10136 — What's new in SwiftUI (2024) @FocusState ใช้ระบบ preference keys ของ SwiftUI เพื่อส่งข้อมูลโฟกัสระหว่างองค์ประกอบ เมื่อช่องได้รับโฟกัส SwiftUI จะอัปเดตคุณสมบัติ @FocusState ที่เกี่ยวข้องโดยอัตโนมัติ ทำให้คุณสามารถตอบสนองต่อการเปลี่ยนแปลงโฟกัสในโค้ดได้
วิธีที่ง่ายที่สุดในการใช้ @FocusState คือกับประเภท Bool เมื่อช่องอยู่ในโฟกัส คุณสมบัติจะเป็น true เมื่อเสียโฟกัส — false คุณสามารถบังคับโฟกัสโดยการตั้งค่าเป็น true หรือลบโฟกัสโดยการตั้งค่าเป็น false
struct LoginForm: View {
@State var email = ""
@FocusState var isEmailFocused: Bool
var body: some View {
VStack {
TextField("Email", text: $email)
.focused($isEmailFocused)
Button("แสดงแป้นพิมพ์") {
isEmailFocused = true
}
Button("ซ่อนแป้นพิมพ์") {
isEmailFocused = false
}
}
}
}
ในตัวอย่างนี้ isEmailFocused จะกลายเป็น true โดยอัตโนมัติเมื่อผู้ใช้แตะที่ช่องข้อความ และเป็น false เมื่อซ่อนแป้นพิมพ์ ปุ่มต่างๆ ช่วยให้คุณจัดการโฟกัสได้โดยทางโปรแกรม — มีประโยชน์สำหรับแป้นพิมพ์ที่กำหนดเอง ปุ่ม "ถัดไป" และสถานการณ์ที่คุณต้องการบังคับซ่อนแป้นพิมพ์หลังจากการส่งฟอร์ม
สำหรับฟอร์มที่มีหลายช่อง @FocusState รองรับ enum ที่สอดคล้องกับโปรโตคอล FocusStateValue (หรือ Hashable) แต่ละ case ของ enum สอดคล้องกับช่องที่เฉพาะเจาะจง ซึ่งช่วยให้สามารถสลับโฟกัสระหว่างช่องได้ — ตัวอย่างเช่น เมื่อผู้ใช้กด "ถัดไป" บนแป้นพิมพ์เพื่อไปยังช่องถัดไป
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() }
}
}
}
สังเกตตัวปรับแต่ง .onSubmit — มันจะถูกเรียกเมื่อผู้ใช้กด "Return" บนแป้นพิมพ์ ภายใน .onSubmit เราสลับ focusedField ไปยังช่องถัดไป ซึ่งจะย้ายโฟกัสโดยอัตโนมัติ ช่องสุดท้ายเรียก submitForm() เพื่อส่งฟอร์ม
@FocusState มีวิธีง่ายๆ ในการซ่อนแป้นพิมพ์ — เพียงตั้งค่าคุณสมบัติเป็น nil (สำหรับ enum) หรือ false (สำหรับ Bool) อย่างไรก็ตาม บางครั้งคุณจำเป็นต้องซ่อนแป้นพิมพ์โดยไม่ผูกกับช่องที่เฉพาะเจาะจง — ตัวอย่างเช่น เมื่อแตะบนพื้นที่ว่าง ในกรณีนี้ มีหลายแนวทาง
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("เสร็จ") {
isFocused = false
}
}
}
}
}
ตัวปรับแต่ง .toolbar กับ placement .keyboard จะเพิ่มปุ่มเหนือแป้นพิมพ์ นี่คือรูปแบบ UX มาตรฐานใน iOS สำหรับการซ่อนแป้นพิมพ์ แนวทางอื่นคือการใช้ .onTapGesture บน VStack หลักเพื่อรีเซ็ตโฟกัสเมื่อแตะบนพื้นหลัง
@FocusState ผสานรวมกับารตรวจสอบความถูกต้องของฟอร์มได้อย่างดีเยี่ยม รูปแบบทั่วไป: หลังจากกดปุ่ม "ส่ง" ให้ตรวจสอบความถูกต้องของทุกช่องและตั้งค่าโฟกัสไปที่ช่องแรกที่มีข้อผิดพลาด ซึ่งช่วยปรับปรุงประสบการณ์ผู้ใช้ — ผู้ใช้จะเห็นทันทีว่าช่องใดต้องแก้ไข
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("ส่ง") { validateAndSubmit() }
}
}
func validateAndSubmit() {
if name.isEmpty {
focusedField = .name
return
}
if phone.isEmpty {
focusedField = .phone
return
}
// ส่งฟอร์ม
}
}
ในตัวอย่างนี้ หากช่อง name ว่างเปล่า โฟกัสจะย้ายไปที่ช่องนั้น และผู้ใช้จะเห็นทันทีว่าข้อผิดพลาดอยู่ที่ไหน หาก name ถูกกรอกแล้ว จะตรวจสอบ phone นี่คือพฤติกรรมตามธรรมชาติสำหรับฟอร์ม — ผู้ใช้กรอกช่องจากบนลงล่าง และการตรวจสอบความถูกต้องเป็นไปตามลำดับเดียวกัน
ข้อผิดพลาดที่พบบ่อยที่สุดคือการพยายามใช้ @FocusState กับประเภทที่ไม่สอดคล้องกับ Hashable @FocusState ต้องการให้ประเภทของคุณสมบัติเป็น Hashable (Bool และ enum แบบทางเลือกสอดคล้องอยู่แล้ว) หากคุณกำลังพยายามใช้โครงสร้างที่กำหนดเอง ตรวจสอบให้แน่ใจว่ามัน implement Hashable
// ❌ ผิด: @FocusState Bool สองตัวแทน enum
@FocusState var isNameFocused: Bool
@FocusState var isEmailFocused: Bool
// ✅ ถูกต้อง: enum @FocusState เดียว
enum Field: Hashable { case name; case email }
@FocusState var focusedField: Field?
คำถามที่พบบ่อย
@FocusState พร้อมใช้งานตั้งแต่ iOS 15, iPadOS 15, macOS 12, tvOS 15 และ watchOS 8 สำหรับโปรเจกต์ที่รองรับ iOS 14 และต่ำกว่า ให้ใช้ UIViewRepresentable กับ UITextField และ becomeFirstResponder หรือไลบรารีของบุคคลที่สามที่มีการจัดการโฟกัสแบบกำหนดเอง
ได้ คุณต้อง implement การรองรับ FocusState ใน UIViewRepresentable ที่กำหนดเองผ่านโปรโตคอล UIViewRepresentable View ที่กำหนดเองต้องมี becomeFirstResponder และ resignFirstResponder SwiftUI จะเชื่อมโยง @FocusState กับเมธอดเหล่านี้โดยอัตโนมัติหากคุณระบุตัวปรับแต่ง .focused()
ใน List หรือ Form เซลล์อาจถูกนำมาใช้ซ้ำ ซึ่งจะทำลายการเชื่อมโยง @FocusState กับช่อง วิธีแก้ไข: เพิ่มตัวปรับแต่ง .id() ด้วยตัวระบุที่ไม่ซ้ำกันสำหรับแต่ละ TextField ตัวอย่าง: .id(fieldName) ซึ่งบังคับให้ SwiftUI สร้างอินสแตนซ์ View แยกต่างหากสำหรับแต่ละช่อง
เพิ่ม .onTapGesture ไปยังคอนเทนเนอร์หลัก (VStack, ZStack) และรีเซ็ตโฟกัส: focusedField = nil อย่างไรก็ตาม .onTapGesture อาจบล็อกการแตะบนปุ่มภายใน — ให้ใช้คอนเทนเนอร์กับ .contentShape(Rectangle()) และ .onTapGesture บนนั้น หรือ UIKitBackgroundView ที่กำหนดเอง
@FocusState ไม่มี API โดยตรงสำหรับแอนิเมชันแป้นพิมพ์ — นี่คือพฤติกรรมของระบบ iOS อย่างไรก็ตาม คุณสามารถตอบสนองต่อการเปลี่ยนแปลงโฟกัสด้วย .onChange(of: focusedField) หรือ .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) สำหรับแอนิเมชันเนื้อหาแบบกำหนดเอง
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม