ignoresSafeArea の第一引数と SwiftUI のキーボード避け

## 概要

SwiftUI には `ignoresSafeArea` という modifier がありますが、しっかり理解せず使っていたので基本的な動作や引数の役割、SwiftUI の自動でのキーボード避けとの関係について調べてまとめました。

## ignoresSafeArea とは

SwiftUI の View はデフォルトでは SafeArea の中に配置されます。例えば画面全体に背景色を設定しようとすると、背景色は以下のように SafeArea 内だけに適用されます。

```swift
struct ContentView: View {
    var body: some View {
        Color.yellow
    }
}
```
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/0fb502e1-f0af-559c-1a6e-6768f0bceca2.png">

ときにはこの振る舞いが余計なお世話になってしまうこともあるでしょう。たとえば、上の画面では背景色の黄色が SafeArea 外にも適用されている方が自然な感じがします。このようなときに `ignoresSafeArea` が使えます。今回の `Color` のように可能な限り広い領域を占有しようとする性質を持つ View に `ignoresSafeArea` が適用されると SafeArea の制限を無視して表示されるようになります。

```swift
struct ContentView: View {
    var body: some View {
        Color.yellow
            .ignoresSafeArea()
    }
}
```

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/1509b985-d6d8-a4e7-887d-61b03799b3dc.png">

引数を何も指定しないと全ての SafeArea を無視しますが、`edges` パラメータを指定することで特定の方向の SafeArea だけ無視することも可能です。たとえば、以下のようにすると画面下側の SafeArea だけ無視するようにできます。

```swift
struct ContentView: View {
    var body: some View {
        Color.yellow
            .ignoresSafeArea(edges: [.bottom])
    }
}
```

<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/bf4d772b-4680-1480-7229-e2a093d36894.png">

## SwiftUI のキーボード避け

ここで、急に画面にテキストフィールドを追加したくなったとしましょう。以下のようなコードになりそうです。

```swift
struct ContentView: View {
    @State private var inputText: String = ""
    
    var body: some View {
        ZStack {
            Color.yellow
            
            VStack {
                Spacer()
                
                TextField("急に追加されたテキストフィールド", text: $inputText)
                    .padding()
                    .background(Color.white)
                    .padding(.bottom, 100)
            }
        }
        .ignoresSafeArea(edges: [.bottom])
    }
}
```

しかし、テキストフィールドに実際に入力しようとすると上からキーボードが被さってきてしまいます。

![keyboard_cover.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/3b0bd5ba-f52e-9ee9-5b2e-2a7c67f9b60a.gif)

以前の iOS 開発であれば画面がキーボードを避ける実装を自前でしなければいけないのが当たり前でしたが、iOS 14 から SwiftUI を使っていれば自動でキーボード避けをしてくれるはずです。それなのになぜ上の実装ではキーボードを避けてくれないのでしょうか。

そもそも SwiftUI のキーボード避けはどのように実現されているかを整理します。実際の実装方法は想像するしかありませんが、自分は

- キーボードが SafeArea 外として扱われる
- SwiftUI の View は SafeArea の内部

という２つが組み合わさって View がキーボードを避けてくれるのだと理解しています（間違っているぞ！という方はコメントください）。キーボードが表示されると SafeArea の高さが増えることは、以下のように `GeometryReader` を追加することで確認できます。

```swift
struct ContentView: View {
    @State private var inputText: String = ""
    
    var body: some View {
        GeometryReader { geometry in
            ZStack {
                Color.yellow

                Text("bottom safe area: \(geometry.safeAreaInsets.bottom)")

                VStack {
                    Spacer()

                    TextField("急に追加されたテキストフィールド", text: $inputText)
                        .padding()
                        .background(Color.white)
                        .padding(.bottom, 100)
                }
            }
            .ignoresSafeArea(edges: [.bottom])
        }
    }
}
```

![safearea_height.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/a44c9081-7ce9-0caa-1a08-5cebf5d6e68f.gif)

上記のテキストフィールドの例では、`.ignoresSafeArea(edges: .bottom)` で View 全体に画面下部の SafeArea を無視するように指示しているので、まさに画面下部の SafeArea であるキーボードを避けてくれなくなったということになります。

## `ignoresSafeArea` の第一引数: `SafeAreaRegions`

それでは SafeArea の外側まで View を広げたいけどキーボードは避けてほしいというときはどうすればいいのでしょうか。冷静に `ignoresSafeArea` のシグネチャを見ると第一引数に `SafeAreaRegions` を指定するようになっています。

https://developer.apple.com/documentation/swiftui/view/ignoressafearea(_:edges:)

その `SafeAreaRegions` のドキュメントを見ると、 SafeArea の領域を表すものであり、以下の３つの static プロパティを持つことがわかります。

- `.container`: デバイスの種類などによって決まる画面上部や下部の領域のこと。我々が普段 SafeArea として意識するのはこちら
- `.keyboard`: View に被さってくるソフトウェアキーボードの領域のこと
- `.all`

`.all` には `.container` と `.keyboard` の両方が含まれることになります。`ignoresSafeArea` ではデフォルトで `.all` が設定されているのでいずれの SafeArea も無視することになるのですが、`.container` を指定するとキーボードによる SafeArea は無視しなくなる、すなわちキーボード避けの挙動を維持してくれるようになります。

```swift
struct ContentView: View {
    @State private var inputText: String = ""
    
    var body: some View {
        ZStack {
            Color.yellow
            
            VStack {
                Spacer()
                
                TextField("急に追加されたテキストフィールド", text: $inputText)
                    .padding()
                    .background(Color.white)
                    .padding(.bottom, 100)
            }
        }
        .ignoresSafeArea(.container, edges: [.bottom]) // .container を指定
    }
}
```

![keyboard_avoid.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/159260/e37f79fa-cc12-1f73-4a4d-3e2b92c0017e.gif)

あまり使うことはなさそうですが、逆に `.ignoresSafeArea(.keyboard)` と指定すれば View を SafeArea 内におさめつつ自動でのキーボード避けを無効化することができます。


## まとめ

- SwiftUI の View はデフォルトでは SafeArea 内に配置されるが、`ignoresSafeArea` を使うとそれを無視して SafeArea 外に View をはみ出させることができる
- `SafeAreaRegions` という構造体が SafeArea の領域を表し、デバイスによって決まる画面上部や下部の SafeArea 外の領域を表す `.container`、ソフトウェアキーボードを表す `.keyboard`、その両方を含む `.all` がある
- `ignoresSafeArea` にはどの `SafeAreaRegions` を無視するかを第一引数で渡せるが、デフォルト値は `.all` なので何も指定しないと `.keyboard` も無視されて自動でキーボードを避けてくれなくなる。この問題は明示的に `.container` を指定すると解決する

## WWDC21 `Add rich graphics to your SwiftUI app`

[コメント](https://qiita.com/maiyama18/items/840258cbd3bc3f10b93c#comment-f6927ab6e6439bdbfc10) で教えていただいたのですが、以下のセッションの冒頭で SafeArea について詳しく説明されているのでぜひ参考にしてみてください。この記事で書いている container / keyboard それぞれによる SafeArea についても触れられています。

https://developer.apple.com/videos/play/wwdc2021/10021

## 参考

- [https://developer.apple.com/documentation/swiftui/view/ignoressafearea(_:edges:)](https://developer.apple.com/documentation/swiftui/view/ignoressafearea(_:edges:))
- [https://developer.apple.com/documentation/swiftui/safearearegions](https://developer.apple.com/documentation/swiftui/safearearegions)
- [https://swiftwithmajid.com/2021/11/03/managing-safe-area-in-swiftui/](https://swiftwithmajid.com/2021/11/03/managing-safe-area-in-swiftui/)
- [https://www.fivestars.blog/articles/swiftui-keyboard/](https://www.fivestars.blog/articles/swiftui-keyboard/)
- https://stackoverflow.com/questions/57116723/how-to-access-safe-area-size-in-swiftui
- https://www.hackingwithswift.com/quick-start/swiftui/how-to-place-content-outside-the-safe-area