SwiftUIでキーボードによる入力が必要になる画面では、キーボードによってコンテンツが隠れてしまわないように気を遣うと思います。
本記事では、キーボードによってコンテンツが隠れないようにする方法を紹介します。
デフォルトの挙動
SwiftUIはデフォルトでは、キーボードが表示されるとその分コンテンツも上に上がり、キーボードに被らないような挙動になります。
struct SampleView: View {
@State var text = ""
var body: some View {
VStack {
ForEach(1...20, id: \.self) {
Text("value: \($0)")
}
Spacer()
TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.padding(.bottom, 16)
}
.padding(.horizontal, 16)
}
}
上記のように、テキストフィールドをフォーカスしてキーボードが表示されると、それに合わせて隠れるはずのテキストフィールドもにゅっと見える位置まで表示されます。
そのため、何も考えていなくてもキーボードによってコンテンツが隠れてしまうという挙動を回避できていることもあります。
デフォルトの挙動だとコンテンツが隠れてしまうケース
ただ、デフォルトの挙動でもコンテンツが隠れてしまうケースもあります。
struct SampleView: View {
@State var text = ""
var body: some View {
VStack {
ForEach(1...20, id: \.self) {
Text("value: \($0)")
}
Spacer()
TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.padding(.bottom, 16)
}
+ .ignoresSafeArea()
.padding(.horizontal, 16)
}
}
セーフエリアを無視して画面全体に表示したいという時に使う.ignoresSafeArea()
をつけると、キーボードが表示されてもテキストフィールドは見える位置まで移動してくれません。
対処法
この対処法として、ignoresSafeArea
モディファイアの第一引数に.container
と指定してやれば良いです。
struct SampleView: View {
@State var text = ""
var body: some View {
VStack {
ForEach(1...20, id: \.self) {
Text("value: \($0)")
}
Spacer()
TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.padding(.bottom, 16)
}
- .ignoresSafeArea()
+ .ignoresSafeArea(.container)
.padding(.horizontal, 16)
}
}
ignoresSafeArea
モディファイアで無視できるセーフエリアには以下2種類あり、ignoresSafeArea
に何も引数を指定しない場合は、以下両方の領域を無視することになります。
そのため、.ignoresSafeArea(.container)
としてkeyboard
の領域は無視しないよと明示的に指定してやることで、従来通りキーボードの領域を認識してテキストフィールドが隠れないようになります。
テキストフィールド以外のコンテンツもキーボードに隠れないようにしたい
デフォルトの挙動では、キーボードの入力先の要素がキーボードに隠れないようにしてくれるだけです。
例えば、キーボードの下にあるボタンをキーボードに隠れないようにしたい場合は、デフォルトの挙動では物足りません。
struct SampleView: View {
@State var text = ""
var body: some View {
ScrollView {
VStack {
ForEach(1...100, id: \.self) {
Text("value: \($0)")
}
Spacer()
TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.padding(.bottom, 16)
Spacer()
.frame(height: 50)
Button("Button") {
print("tapped button")
}
.buttonStyle(.bordered)
.background(.red)
}
}
.padding(.horizontal, 16)
}
}
対処法
上記の例で、キーボード表示時にボタンまで隠れないようにするのは、ScrollViewReader
を使ってキーボード表示後にボタンが見える位置までスクロールすることで実現できます。
この方法は以下記事を参考にさせていただきました!
struct SampleView: View {
@State var text = ""
private static let buttonId = "buttonId"
var body: some View {
ScrollViewReader { proxy in
ScrollView {
VStack {
ForEach(1...100, id: \.self) {
Text("value: \($0)")
}
Spacer()
TextField("Text", text: $text)
.textFieldStyle(.roundedBorder)
.padding(.bottom, 16)
.ignoresSafeArea(.keyboard)
Spacer()
.frame(height: 50)
Button("Button") {
print("tapped button")
}
.buttonStyle(.bordered)
.background(.red)
.id(Self.buttonId)
}
}
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
Task {
try await Task.sleep(for: .seconds(0.1))
withAnimation {
proxy.scrollTo(Self.buttonId, anchor: .bottom)
}
}
}
}
.padding(.horizontal, 16)
}
}
ポイントとして、まず以下コードで、キーボードの表示のタイミングを検知し、ScrollViewReader
から取得したScrollViewProxy
で任意の位置までスクロールを行います。
また、キーボードを表示を検知してすぐだとスクロール処理がうまく機能しない場合があったので、0.1秒の遅延処理を入れています。
おそらく、デフォルトでテキストフィールドを表示しようとする挙動と、衝突してうまくいかないのかなと思っていますが、この辺り詳しい方いたら教えて欲しいです🙏
.onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardDidShowNotification)) { _ in
Task {
try await Task.sleep(for: .seconds(0.1))
withAnimation {
proxy.scrollTo(Self.buttonId, anchor: .bottom)
}
}
}
onReceive
で監視するイベントをkeyboardWillShowNotification
にしてしまうと、キーボード表示前でキーボード分のセーフエリアが追加されていないので、さらに遅延処理が必要になるかもです。
おわり
以上が、私が知っているキーボードの表示によってコンテンツが隠れるのを防ぐ方法でした!
他にも良い方法があれば、共有してもらえると嬉しいです!