이 포스팅에서 FocusState를 다루는 방법을 배웠었는데요. 아래 코드처럼 View에 Binding으로 연결해서 사용했습니다. FocusState로 선언된 변수가 equals와 동일한 값으로 바뀌면 해당 View로 focus를 변경해주는 기능이었습니다.
@FocusState private var isFrontFinishEditing: Bool
TextEditor(text: $viewModel.backText)
.font(.system(size: 30))
.frame(height: Constants.Size.deviceHeight / 8)
.padding(.horizontal)
.focused($isFrontFinishEditing, equals: true)
하지만 View 계층이 복잡해지면서 TextEditor가 @FocusState 변수가 있는 View에 있지 않고 그 하위 View에 있게 되는 일이 생길 수도 있습니다. 이 경우에는 어떻게 해야 할까요?
@FocusState private var editFocus: InputType?
// MARK: Body
var body: some View {
ScrollView {
VStack {
WordBookPickerView()
VStack {
TextInputView(inputType: .meaning) //👉 TextEditor가 있는 뷰
ImageInputView(inputType: .meaning)
}
.padding(.bottom)
VStack {
TextInputView(inputType: .gana) //👉 TextEditor가 있는 뷰
ImageInputView(inputType: .gana)
}
VStack {
TextInputView(inputType: .kanji) //👉 TextEditor가 있는 뷰
ImageInputView(inputType: .kanji)
}
SaveButton()
}
}
}
.focused()는 @FocusState의 Binding을 인자로 받습니다. 따라서 FocusState의 Binding 참조를 initializer를 통해서 상위 View에서 하위 View로 전달하면 됩니다.
TextInputView(inputType: .meaning, editFocus: $editFocus)
그 다음에는 하위 View에서 해당 참조를 활용해서 .focused()를 TextEditor에 적용하면 됩니다. 다만 타입에 주의할 필요가 있습니다. 단순하게 FocusState<InputType?>가 아니라 $를 붙여서 Binding 참조를 전달했으므로 최종적으로 타입은 FocusState<InputType?>.Binding가 됩니다.
struct TextInputView: View {
private let inputType: InputType
@EnvironmentObject private var viewModel: ViewModel
private var editFocus: FocusState<InputType?>.Binding
init(inputType: InputType, editFocus: FocusState<InputType?>.Binding) {
self.inputType = inputType
self.editFocus = editFocus
}
var body: some View {
Text("\(inputType.description) 입력")
.font(.system(size: 20))
TextEditor(text: bindingText)
.font(.system(size: 30))
.frame(height: Constants.Size.deviceHeight / 8)
.padding(.horizontal)
.focused(editFocus, equals: inputType) //👉 focused로 적용
}
}
이 방법은 약간 복잡합니다… FocusState<InputType?>.Binding라는 복잡한 타입을 써야하고요. 그냥 상위 View에 focused를 붙여버리면 안될까요?
아래 코드처럼 상위 View에 적용하면 안될까요? 뭔가 안될 것 같은 느낌이 들지만 놀랍게도 가능한 방법입니다. 이렇게 하는 경우 TextInputView 안에 있는 View 요소들 중에서 First Responder가 될 수 있는 첫 번째 View가 focus 되게 됩니다.
🧪 실제로 TextInputView 안에 여러개의 TextEditor를 넣고 실험을 해보면 가장 첫번째 TextEditor가 First Responder가 되는 것을 볼 수 있습니다.
var body: some View {
ScrollView {
VStack {
WordBookPickerView()
VStack {
TextInputView(inputType: .meaning)
.focused($editFocus, equals: .meaning)
ImageInputView(inputType: .meaning)
}
.padding(.bottom)
VStack {
TextInputView(inputType: .gana)
.focused($editFocus, equals: .gana)
ImageInputView(inputType: .gana)
}
VStack {
TextInputView(inputType: .kanji)
.focused($editFocus, equals: .kanji)
ImageInputView(inputType: .kanji)
}
SaveButton()
}
.padding(.top, 50)
.onAppear { viewModel.getWordBooks() }
.onChange(of: viewModel.meaningText) { moveCursorToGanaWhenTap($0) }
.onChange(of: viewModel.ganaText) { moveCursorToKanjiWhenTap($0) }
}
}
방법 2가 훨씬 간단하고 좋은 방법이라고 생각합니다. 하지만 만약 하위 View에 2개 이상의 focused 대상이 있다면 첫번째 방법을 사용하는 방법 밖에는 없을 것입니다.