SafeAreaについてまとめ

Swift愛好会 Advent Calendar 2017の18日目担当の @gentlejkovです。
本日はiOS11から追加されたSafeAreaについてまとめます。

## SafeArea概要
特殊な形状をした新端末iPhoneXが発売されたことで、iOS11から複雑な画面エッジに対応するためにSafeAreaという概念が追加されました。
SafeAreaはUIView内に操作可能なUIコントロールや意味のある情報を配置すべき領域を提示しています。SafeAreaの領域外にボタンやスライダーなどのUIComponentや重要な情報を配置すると、iPhoneXのジェスチャーに干渉したり、ディスプレイが丸いために4隅の情報が隠れてしまったりといった問題が発生します。

## SafeAreaの領域
iPhoneXのSafeAreaの領域が下記になります。白い部分がSafeArea内部の領域です。上に`44px`, 下に`34px`分SafeAreaの領域外となります。
<img width="415" alt="iPhoneX Portlate" src="https://qiita-image-store.s3.amazonaws.com/0/178769/59cfbed0-1e3e-09f2-aec2-c90b606c83a9.png">

Landscapeの場合は、左右`44px`, 下に`21px`がSafeAreaの領域外となります。
![iPhoneX Landscape](https://qiita-image-store.s3.amazonaws.com/0/178769/f60303b9-bd2d-2ba9-6a8f-2f219638a3eb.png)

ついでにiPhoneX以外のiPhone, とiPadも確認しておきましょう。
iPhoneはPortraitで上`20px`, Landscapeの場合はステータスバーが消えるためSafeAreaと画面の領域が一致します。

<img width="415" alt="iPhone8(P).png" src="https://qiita-image-store.s3.amazonaws.com/0/178769/9da84b2d-910f-b401-88b9-17484c26e219.png">

![iPhone8(L).png](https://qiita-image-store.s3.amazonaws.com/0/178769/ea651b55-9e47-d4a6-bde0-60b5921c7d8a.png)

iPadはPortrait, Landscape共に上`20px`となります。もちろん、ステータスバーの表示設定を変更している場合はこの通りではありません。
<img width="600" alt="iPad(P).png" src="https://qiita-image-store.s3.amazonaws.com/0/178769/2c2c6d92-13e2-77f9-a61c-1653a91ccb69.png">

![iPad(L).png](https://qiita-image-store.s3.amazonaws.com/0/178769/ef7caab4-9922-458b-5b2e-9615a8bcedee.png)

## SafeAreaに対応させるには
AutoLayoutを利用してSafeAreaに対応させる場合は、InterfaceBuilderから`Use Safe Area Layout Guides`にチェックを入れましょう。これはStorybordのみならず、xlibでも利用できます。
<img width="260" alt="スクリーンショット 2017-12-18 21.52.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/178769/e2c755d2-bc85-a5cf-3897-1b4a137def74.png">
UIViewController内部に配置したViewとSafeAreaとの間に制約を貼ることで、ViewをSafeArea内部に限定して配置することができます。
<img width="743" alt="スクリーンショット 2017-12-18 21.56.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/178769/17f37d4e-c7f4-0fac-fa34-e65a3faa58ca.png">
UIViewControllerに紐づくUIViewが背景のViewになり、SafeArea外の領域ではそちらが表示されるため、背景色を設定しておくことを忘れないようにしましょう。

また、背景にぼかし画像を設定したり、全面にUIMapViewを配置するなど、SafeArea領域外も含む領域に新たにUIViewを設定したい場合があります。その際は、UIViewを設置してSafeAreaに対してAutoLayoutを設定した後、各AutoLayoutのSafeAreaのItemをSuperviewに変更するという手順で設定するのが良いかと思います。
<img width="1130" alt="スクリーンショット 2017-12-18 22.04.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/178769/bdfa2ac1-08ec-59a4-95e5-a638088ed2fa.png">

ソースコード中でSafeAreaの領域を取得したい場合、UIViewの`safeAreaInsets`プロパティを使用することができます。このプロパティは該当のViewの領域でSafeArea範囲外に当たるInsetsを提示します。Viewの位置や階層によって自動で範囲を再計算してくれるため、非常に便利なプロパティです。ただ、レイアウト確定後のにこちらのプロパティが計算されるため、`viewDidLoad`などレイアウト確定前のタイミングではこのプロパティには数値が入らないため、注意しましょう。

## 自動でSafeAreaに対応してくれるUIComponentたち
いくつかのUIComponentは特別な対応なしでSafeAreaに自動対応してくれます。特別な対応なしでSafeAreaに対応してくれるため非常に便利なので、まとめておきましょう。

### UINavigationBar
まずは上下のバーです。UINavigationControllerはみなさん使っているため、問題ないかと思います。iPhoneXの場合はUINavigationBarのサイズが上方向に拡大し、SafeAreaの領域を覆ってくれます。また、UINavigationController配下のUIViewControllerから見ると、UINavigationBarの領域分がSafeArea範囲外となります。
なお、SafeAreaとは直接関係ありませんが、Xcode9.0, 9.1の頃、UINavigationBarを表示している画面から表示していない画面に戻る際、戻るアニメーションが問題のある動作をしておりました。Xcode8.xの頃は戻る動作中UINavigationBarがフェイドアウトする動作だったのが、Xcode9.0, 9.1では戻りのアニメーションが完了するまでUINavigationBarが表示され続け、戻りが完了した瞬間にUINavigationBarが消えて画面全体が上にずれるような動作になってしまいました。改めて今回Xcode9.2で検証してみるとこの動作は直っているようで、戻る動作中UINavigationBarごと画面が右に消えていく動作になっております。

### UIToolBar
UIToolbarのbottomとSafeArea.bottomを一致させる制約を貼っておくと、iPhoneXの場合にUIToolBarが下方向に拡張し、SafeAreaの範囲外をUIToolbarで埋めてくれます。
ただし、この場合、UIToolBar自体のframeの高さは変わっていないようです。そのため、UIToolBarを上下に出し入れするアニメーションを入れる場合、UIToolBarの高さ分だけ位置を変更するとUIToolBarが途中までしか移動しないため、注意してください。

<img width="415" alt="iPhoneX Portlate" src="https://qiita-image-store.s3.amazonaws.com/0/178769/e50e07e1-cd84-407e-461e-111efaa89a98.png">

### UITableView
UITableViewは特に何も考えなくとも自動的にSafeAreaに対応してくれるUIComponentの一つです。下記はUITableViewControllerのみの画面のサンプルです。UITableViewはSafeAreaの外まで一杯に広がっていますが、上部と下部のSafeArea外領域は`safeAreaInsets`で自動的に正しい表示領域を設定してくれます。この場合でも、contentInsetsは全て0になっています。

<img width="415" alt="iPhoneX Portlate" src="https://qiita-image-store.s3.amazonaws.com/0/178769/7ecf4f8e-ff16-8ec6-ae4a-745c12fe1447.png">

<img width="415" alt="iPhoneX Portlate" src="https://qiita-image-store.s3.amazonaws.com/0/178769/b756664c-a918-3991-f4c7-b59775f2d852.png">

また、Landscapeモードの場合も同様に左右のSafeAreaに沿ってCell内のContentViewが自動的に設定されるため、通常はSafeAreaの外にCellのコンテンツが表示されることはありません。
![iPhoneX(L).png](https://qiita-image-store.s3.amazonaws.com/0/178769/7d114c58-c81a-0d49-5fe4-aae5ba9ddf72.png)

逆に、Cellの背景色や背景画像を設定しようとした場合、ContentViewの背景色のみを設定したり、UIImageViewとContentViewとの間で制約を設定してしまうと、左右SafeArea外の背景はUITableViewそのままになってしまいます。その場合は、Cell自体のViewの背景色を設定したり、ContentViewに乗せたUIImageViewとCell自体の間でAutoLayoutの制約を設定するとCell全体に背景色や背景画像を設定することができます。
<img width="456" alt="スクリーンショット 2017-12-19 1.25.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/178769/c3815ee3-4530-674d-98f5-cf73984ed2db.png">
上記設定はIntefaceBuilder上では一見全く見分けがつかないため気をつけましょう。

### SafeArea対応BottomBar
SafeAreaに対応した自作のBottomBarの作成方法の一例を紹介します。
まず、BottomBarはVerticalのUIStackViewで作成します。UIStackViewには上下に2つUIViewを設定し、UIStackViewの左右と下がSuperViewに接地するようAutoLayoutを設定します。UIStackView中の上のUIViewに高さの制約を設定します。この高さがBottomBarの実質的な高さになります。
そして、上のUIViewのBottomとSafeAreaのBottomの間に制約を設定します。これで、下のSafeAreaの幅に従って下のViewの高さが自動的に変わるようになります。
Landscapeにも対応させる場合は、上のUIViewの中にさらにUIViewを設定し、そのUIViewの左右とSafeAreaの左右に制約を設定します。ボタンなどのUIComponentは中のUIView中に設置するようにしていけば、iPhoneXのLandscape時に左右のSafeArea内にUIComponentが収まるようになります。

<img width="591" alt="スクリーンショット 2017-12-19 2.20.12.png" src="https://qiita-image-store.s3.amazonaws.com/0/178769/7fc069ce-f9bb-4580-18be-a47f1e0212bd.png">

## 最後に
以上、SafeAreaについて簡単にまとめてみました。
なお、私はiPhoneXの実機を持っていないため、全てシミュレータ上での検証のみであることをご了承ください。