😬

[SwiftUI] Listの中にUITextViewを配置したとき、ClearButtonがタップできない問題

に公開

要約

SwftUI.Listで、UIViewRepresentableを使用してUITextViewを配置できる。
しかし、iOS16,17で、ListにonTapGestureを追加すると、UITextViewのClearButtonがタップできなくなる。
2つの回避策を共有する。

環境

  • iOS 16〜18
  • Xcode16.1, 16.2

背景

以下のコードを書く

  • Listの中にUITextViewを配置する
  • UITextView以外の場所をタップすると、UITextViewのキーボードを閉じる

UIViewRepresentableを使用する

  • SwiftUIからUITextViewを利用するために、UIViewRepresentableを使用する。
struct UITextFieldContainer: UIViewRepresentable {
    @Binding var text: String
    func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextField {
        let view = UITextField()
        view.clearButtonMode = .whileEditing
        view.delegate = context.coordinator
        return view
    }
    
    func updateUIView(_ uiView: UITextField, context: UIViewRepresentableContext<Self>) {
        uiView.text = text
    }
    
    final class Coordinator: NSObject, UITextFieldDelegate {
        private var textView: UITextFieldContainer
        init(_ textView: UITextFieldContainer) {
            self.textView = textView
            super.init()
        }
        
        func textFieldDidChangeSelection(_ textField: UITextField) {
            textView.text = textField.text ?? ""
        }
        
        func textFieldShouldClear(_ textField: UITextField) -> Bool {
            self.textView.text = ""
            return true
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
}

ListにUITextViewを配置する

  • 上記のUITextFieldContainerを使用して、ListにUITextViewを配置する。
  • UITextView以外の場所をタップすると、UITextViewのキーボードを閉じる
struct ContentView: View {
    enum FocusField {
        case textField
    }
    
    @FocusState var focusField: FocusField?
    @State var text: String = ""
    var body: some View {
        VStack {
            List {
                Section(header: Text("TextField1")) {
                    UITextFieldContainer(text: $text)
                        .focused($focusField, equals: .textField)
                }
            }
            .onTapGesture {
                focusField = nil
            }
        }
    }
}

上記コードの問題

  • iOS 18では期待どおりに動く

    • UITextFiledのClearButtonをタップすると、textFieldShouldClearが呼ばれ、textがクリアされる。
    • Listの空白の部分をタップすると、キーボードが閉じる
  • iOS 16,17では、Listの空白の部分をタップしても、キーボードが閉じない

    • ListにonTapGestureを追加すると、UITextViewのclearButtonをタップできなくなる。

Listの代わりにVStackを使用しても、同様の結果になる。

回避策1

  • UITextFiledのclearButtonを自分で実装する。
    • UITextFiledに、overlayclearButtonを追加する
Section(header: Text("TextField2")) {
    UITextFieldContainer(text: $text)
        .overlay(alignment: .trailing) {
            Image(systemName: "xmark.circle.fill")
                .resizable()
                .frame(width: 20, height: 20)
                .opacity(text.isEmpty ? 0 : 1)
                .onTapGesture {
                    self.text = ""
                }
        }
        .focused($focusField, equals: .textField)
}

メリット

  • iOS 16,17では、onTapGestureを追加しても、UITextViewのclearButtonをタップできるようになる。

デメリット

  • iOS 18では、ListのonTapGestureが先に呼ばれてしまい、自作のClearButtonをタップできなくなる。
  • 将来のiOSバージョンやXcodeのバージョンで、この回避策が通用しなくなる可能性がある。

回避策2

  • Listの下部にViewを設置し、そのViewにonTapGestureを追加する。
struct ContentView: View {
    enum FocusField {
        case textField
    }
    
    @FocusState var focusField: FocusField?
    @State var text: String = ""
    var body: some View {
        VStack {
            GeometryReader { proxy in
                List {
                    Section(header: Text("TextField1")) {
                        UITextFieldContainer(text: $text)
                            .focused($focusField, equals: .textField)
                    }
                    Rectangle()
                        .fill(Color.white)
                        .frame(height: proxy.size.height - 100)
                        .onTapGesture {
                            focusField = nil
                        }
                        .listRowSeparator(.hidden)
                }
                .listStyle(.plain)
                .scrollContentBackground(.hidden)
            }
        }
    }
}

メリット

  • iOS 16〜18で、共通の実装を利用できる。

デメリット

  • 下部の空白のサイズを調整しないと、UIが良くない
    • Keyboardが表示されたときの、UIの調整が難しい。
    • Contentsがない部分までスクロールできてしまう。

最後に

最善の解決ではないかもしれないが、ListにonTapGestureを追加すると、UITextFieldのClearButtonがタップできなくなる問題の回避策を共有した。
他にも良い解決策があれば教えてください🙇

SwiftUI むずかしい。。。

Discussion