はじめに
Xcode12、SwiftUIで動作を確認しました。
iOS14のテキストフィールド
従来のアプリ開発において、キーボード入力のある画面では下記のような挙動が発生してしまうケースが度々発生し、キーボードの高さ分だけ表示位置を調整するといった実装を行うことがよくありました。
- TextFieldにフォーカスが当たる
- ソフトウェアキーボードが表示される
- キーボードが表示されたことでTextFieldが見えなくなってしまう

struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
Spacer()
TextField("input here", text: $text)
.padding()
}
}
}
このような状況に対して、iOS14では入力中のTextFieldがキーボードの表示領域外に出てくるように、自動で表示調整がされるようになっています。

これは大変便利な変更ですが、注意が必要な点もあります。
注意点
この自動調整挙動は、iOS14でのみ行われるため、iOS13では表示調整用の実装を従来通り自前で実装する必要があります。
struct ContentView: View {
@State private var text: String = ""
@ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
Spacer()
TextField("input here", text: $text)
.padding()
.padding(.bottom, viewModel.bottomPadding)
}
}
}
class ViewModel: ObservableObject {
@Published var bottomPadding: CGFloat = 0.0
init() {
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillChangeFrameNotification,
object: nil,
queue: OperationQueue.main
) { [weak self] (notification: Notification) -> Void in
guard let userInfo = notification.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
withAnimation {
self?.bottomPadding = keyboardFrame.size.height
}
}
}
}
しかし、ここでOS間の挙動の違いを意識していないと、以下のようにiOS14で想定していない位置にまでTextFieldが移動してしまうことになります。

そのため、iOS13以下をサポートする場合は、OSごとに挙動を切り替えるような実装をする必要がありそうです。
class ViewModel: ObservableObject {
@Published var bottomPadding: CGFloat = 0.0
init() {
if #available(iOS 14, *) {
} else {
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillChangeFrameNotification,
object: nil,
queue: OperationQueue.main
) { [weak self] (notification: Notification) -> Void in
guard let userInfo = notification.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
withAnimation {
self?.bottomPadding = keyboardFrame.size.height
}
}
}
}
}
挙動の違い
最後に、画面構成によってどのような挙動の違いが出るかをまとめておきます。
List
struct ContentView: View {
@State private var text: String = ""
var body: some View {
List {
ForEach(0..<30) {
Text("\($0)")
}
TextField("input here", text: $text)
.padding()
}
}
}

ScrollView
struct ContentView: View {
@State private var text: String = ""
var body: some View {
ScrollView {
ForEach(0..<30) {
Text("\($0)")
}
TextField("input here", text: $text)
.padding()
}
}
}

VStack
十分なスペースがない場合
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
ForEach(0..<30) {
Text("\($0)")
}
TextField("input here", text: $text)
.padding()
}
}
}

表示されないので注意!
十分なスペースがある場合
struct ContentView: View {
@State private var text: String = ""
var body: some View {
VStack {
ForEach(0..<20) {
Text("\($0)")
}
TextField("input here", text: $text)
.padding()
}
}
}
