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

SteadySlower·2022년 8월 25일
0

이 포스팅에서 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
백과사전 보다 항해일지(혹은 표류일지)를 지향합니다.

0개의 댓글

관련 채용 정보