酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

【未解決】SwiftUIのListに配置したTextEditorにフォーカスを当てても自動でスクロールされない

iOS 14になってからTextFieldにフォーカスを当てると適切なポジションに自動でスクロールするようになった。iOS 13ではKeyboardObservingなどのライブラリを利用する必要があったので、標準機能として実装されたのはとても嬉しい。

その一方で、SwiftUIのList上に配置したTextEditorにフォーカスを当てても自動でスクロールされない。

SwiftUIのTextFieldはUIKitでのUITextFieldに相当する1行入力用のViewで、TextEditorはUITextViewに相当する複数行入力用のViewである。

UITextViewをUIViewRepresentableでラッピングしたView(ここでは仮にCustomTextViewとする)にフォーカスを当てても同様に自動でスクロールされない。CustomTextViewでも uiView.becomeFirstResponder() を呼んでいるが自動でスクロールしてくれないようだった。

再現環境

  • Xcode 13.1
  • iPhone X / iOS 14.7
  • iPhone 13 Pro / iOS 15.1

再現コード

最小限のコードを作成してみた。

アプリ固有の設定のせいでおかしいのかと思ったが、導入しているライブラリ等を除外しても発生したのでデフォルトで発生する問題のようだ。

import SwiftUI

struct ContentView: View {
    
    @State private var singleLineText: String = ""
    @State private var multipleLineText: String = ""
    
    var body: some View {
        List {
            Section {
                ForEach(0 ..< 20) { _ in
                    Text("ああああああ")
                }
            }

            Section {
                TextField(
                    "プレースホルダー",
                    text: $singleLineText
                )
                TextEditor(
                    text: $multipleLineText
                )
            }
            
            Section {
                ForEach(0 ..< 20) { _ in
                    Text("ああああああ")
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {

    static var previews: some View {
        ContentView()
    }
}

「プレースホルダー」と表示されたTextFieldにフォーカスを当てると、ソフトキーボードが表示されてListが自動でスクロールされる。

f:id:ch3cooh393:20211113220053p:plain
TextFieldにフォーカスを当てると自動でListがスクロールされる

しかしTextEditorにフォーカスを当てても自動でスクロールしてくれない。

isScrollEnabledをfalseに設定しておくと、フォーカスが当たったタイミングで自動でスクロールしてくれる

UITextViewをUIViewRepresentableでラッピングしたViewの場合は、makeUIView(context:)のタイミングで textField.isScrollEnabled = false に設定しておくと、フォーカスが当たったタイミングで自動スクロールしてくれることがわかった。

SwiftUI-Introspectを使って、TextEditorの内部に使われているUITextViewを取り出して textView.isScrollEnabled = false を指定すれば良いと思うが、将来的にメンテナンスしていくアプリでSwiftUI-Introspectは使いたくない。SwiftUI-Introspectを使った場合はこんな感じで対応できると思う。

TextEditor(text: $multipleLineText)
    .introspectTextView { view in
        view.isScrollEnabled = false
    }

TextEditorの中身を取り出す方法はほかにも「[Swift] SwiftUI の TextView から UITextfield を取り出す - Qiita」もあるが、SwiftUI-Introspectと同様の理由が使いたくない。