@FocusState는 iOS 15에서 도입된 SwiftUI의 property wrapper로, 텍스트 필드 및 기타 요소에서 입력 포커스를 프로그래밍 방식으로 제어할 수 있게 해줍니다. 이 기능이 도입되기 전에는 개발자가 becomeFirstResponder 및 resignFirstResponder 같은 UIKit 메서드에 접근하기 위해 UIViewRepresentable을 사용해야 했습니다. @FocusState는 이 문제를 네이티브하게 해결합니다. .focused() 수정자를 통해 속성을 필드에 바인딩하면, 포커스 설정 또는 해제는 간단한 값 할당으로 이루어집니다. Apple Developer Documentation — FocusState (2025)에 따르면, @FocusState는 두 가지 모드를 지원합니다. 간단한 관리를 위한 Bool(포커스 켜짐 또는 꺼짐)과 여러 필드를 위한 enum으로, 각 케이스는 특정 입력 필드에 해당합니다.
핵심 사항
@FocusState는 SwiftUI에서 포커스 상태를 특정 입력 필드 또는 기타 포커스 가능한 요소에 바인딩하는 property wrapper입니다. becomeFirstResponder 및 resignFirstResponder 메서드를 통해 포커스 관리가 이루어지는 UIKit과 달리, SwiftUI는 선언적 접근 방식을 사용합니다. 상태(@FocusState)를 선언하고 .focused() 수정자를 통해 요소에 바인딩합니다. 상태를 변경하면 자동으로 포커스가 변경됩니다.
iOS 15에서 @FocusState가 도입되기 전에는 개발자가 UITextField 주변에 UIViewRepresentable 래퍼를 만들거나 타사 라이브러리를 사용해야 했습니다. @FocusState는 SwiftUI에 직접 통합되어 있으며 TextField, TextEditor, SecureField 및 SearchField와 함께 작동합니다. 이는 코드를 더 깔끔하게 만들고 UIKit 브리지 수를 줄이며 테스트 용이성을 향상시킵니다.
WWDC Session 10136 — What's new in SwiftUI (2024)에 따르면, @FocusState는 SwiftUI의 preference key 시스템을 사용하여 요소 간에 포커스 정보를 전달합니다. 필드가 포커스를 받으면 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는 FocusStateValue(또는 Hashable) 프로토콜을 준수하는 enum을 지원합니다. 각 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
}
}
}
}
}
placement .keyboard와 함께 .toolbar 수정자는 키보드 위에 버튼을 추가합니다. 이는 iOS에서 키보드를 숨기는 표준 UX 패턴입니다. 대안적인 접근 방식은 루트 VStack에 .onTapGesture를 사용하여 배경을 탭할 때 포커스를 재설정하는 것입니다.
@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이 확인됩니다. 이는 폼의 자연스러운 동작입니다. 사용자는 위에서 아래로 필드를 채우고 유효성 검사도 동일한 순서를 따릅니다.
가장 흔한 실수는 Hashable을 준수하지 않는 타입과 함께 @FocusState를 사용하려는 것입니다. @FocusState는 속성 타입이 Hashable이어야 합니다(Bool 및 옵셔널 enum은 이미 준수함). 사용자 정의 구조체를 사용하려는 경우 Hashable을 구현하는지 확인하세요.
// ❌ 오류: enum 대신 두 개의 @FocusState Bool
@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 이하를 지원하는 프로젝트의 경우 UITextField 및 becomeFirstResponder와 함께 UIViewRepresentable을 사용하거나, 사용자 정의 포커스 관리 구현이 있는 타사 라이브러리를 사용하세요.
네, 사용자 정의 UIViewRepresentable에서 UIViewRepresentable 프로토콜을 통해 FocusState 지원을 구현해야 합니다. 사용자 정의 View에는 becomeFirstResponder와 resignFirstResponder가 있어야 합니다. .focused() 수정자를 지정하면 SwiftUI가 @FocusState를 이러한 메서드에 자동으로 연결합니다.
List 또는 Form에서 셀이 재사용될 수 있으며, 이로 인해 필드와의 @FocusState 바인딩이 깨집니다. 해결 방법: 각 TextField에 고유 식별자와 함께 .id() 수정자를 추가하세요. 예: .id(fieldName). 이렇게 하면 SwiftUI가 각 필드에 대해 별도의 View 인스턴스를 생성합니다.
루트 컨테이너(VStack, ZStack)에 .onTapGesture를 추가하고 포커스를 재설정합니다: focusedField = nil. 그러나 .onTapGesture는 내부 버튼 탭을 차단할 수 있습니다. .contentShape(Rectangle())이 있는 컨테이너와 그 위에 .onTapGesture를 사용하거나, 사용자 정의 UIKitBackgroundView를 사용하세요.
@FocusState는 키보드 애니메이션을 위한 직접적인 API를 제공하지 않습니다. 이는 iOS 시스템 동작입니다. 그러나 .onChange(of: focusedField) 또는 .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification))을 사용하여 사용자 정의 콘텐츠 애니메이션을 위해 포커스 변경에 반응할 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.