@FocusState를 하위 View에서 사용하기

SteadySlower·2022년 8월 25일
0
post-custom-banner

이 포스팅에서 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()
        }
    }
}

방법 1: 하위에 참조를 전달하기

.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를 붙여버리면 안될까요?

방법 2: 상위 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 대상이 있다면 첫번째 방법을 사용하는 방법 밖에는 없을 것입니다.

profile
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.
post-custom-banner

0개의 댓글