iOS14からテキストフィールドがキーボードに隠れるのを自動調整してくれるようになった

## はじめに

Xcode12、SwiftUIで動作を確認しました。

## iOS14のテキストフィールド

従来のアプリ開発において、キーボード入力のある画面では下記のような挙動が発生してしまうケースが度々発生し、キーボードの高さ分だけ表示位置を調整するといった実装を行うことがよくありました。

1. TextFieldにフォーカスが当たる
2. ソフトウェアキーボードが表示される
3. キーボードが表示されたことでTextFieldが見えなくなってしまう

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/e163d62d-5a2d-b6c0-6e96-b8008d3762b6.gif" />

```swift
struct ContentView: View {
    @State private var text: String = ""
    
    var body: some View {
        VStack {
            Spacer()
            TextField("input here", text: $text)
                .padding()
        }
    }
}
```

このような状況に対して、iOS14では入力中のTextFieldがキーボードの表示領域外に出てくるように、自動で表示調整がされるようになっています。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/8c401be7-5117-3001-739e-a5192a35c9bc.gif" />

これは大変便利な変更ですが、注意が必要な点もあります。

## 注意点

この自動調整挙動は、iOS14でのみ行われるため、iOS13では表示調整用の実装を従来通り自前で実装する必要があります。

```swift
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が移動してしまうことになります。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/0855cc0a-eae0-019f-9fff-29a96831f8da.gif" />

そのため、iOS13以下をサポートする場合は、OSごとに挙動を切り替えるような実装をする必要がありそうです。

```swift
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

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

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/671c605c-dce8-f3cf-29c4-ec9e00b8b6fd.gif" />

### ScrollView

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

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/7c6c59a8-b94b-d18a-d399-b999a50c55f5.gif" />


### VStack

#### 十分なスペースがない場合

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

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/4e216cd7-2612-3fa5-772d-c63f6a19eae2.gif" />

:warning: **表示されないので注意！**

#### 十分なスペースがある場合

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

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41486/a2192b78-809b-05b6-d065-18f207e8b94d.gif" />