😬
[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の空白の部分をタップすると、キーボードが閉じる
- UITextFiledのClearButtonをタップすると、
-
iOS 16,17では、Listの空白の部分をタップしても、キーボードが閉じない
- Listに
onTapGesture
を追加すると、UITextViewのclearButtonをタップできなくなる。
- Listに
Listの代わりにVStackを使用しても、同様の結果になる。
回避策1
- UITextFiledのclearButtonを自分で実装する。
- UITextFiledに、
overlay
でclearButton
を追加する
- UITextFiledに、
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