iOSアプリエンジニアの吉岡です。 最近はブームに乗り遅れてスプラトゥーン2を始めたり、ガルパンを見たりしました。
弊社は direct というビジネスチャットを提供しており、そのiOSアプリをiPhone X対応してリリースしました。 対応記事はすでに世の中にあふれているように思いますが、どんなことをしたのかはアプリの事情ごとに異なると思います。似たような事情の方の参考になるかもしれないと思い、私がやったことを書きます。
ちなみに、現状で弊社アプリではStoryboardやxibを使ったViewが少ないこともあり、Interface Builderを使った対応についてはこの記事では触れません。コードベースでレイアウトしているアプリでの対応実例です。
対応前の状況
- 現行の開発はXcode 8.3.xで行っている
- (Swift3のまま)Xcode 9でデバッグできるところまでは確認済み(アーカイブやfastlaneでのビルドは未確認)
- 多くの画面はStoryboardやxibを使っておらず、viewDidLoadでUIView#frameを自分で設定 & Autoresizingによるレイアウト
- UINavigationBarのtranslucentは無効にしている
そのままXcode 9でiPhone Xシミュレータで実行した例が下記のような画面です。画面によっては、左右と底面それぞれのSafeAreaにボタンやコンテンツが食い込んでいる状態で好ましくありません
iPhone X対応とは?
iPhone Xの全画面でアプリを表示できるようにすることです。Xcode 8(iOS 10のSDK)を用いてビルドしたアプリはiPhone Xでは上下に黒い帯がついた状態となってしまいますが、Xcode 9(iOS 11のSDK)を用いてビルドしたアプリでは全画面に表示されます。
しかし、SDKの移行だけで解決するわけではありません。多くの場合はいくつかの対応が必要となります。 Update Apps for iPhone X – iOS – Apple Developer で紹介されているように、ボタンなどのUIはSafe Areaの内側に表示することが求められます。アプリによってはそれ以外にもiOS 11のSDKに移行することでうまく動かなくなるところがあるかもしれません。iPhone X対応をするにはそのあたりの対応がすべて必要になります。
やったことの概要
- Xcode 9への移行
- Swift 4への移行
- UISearchDisplayControllerをUISearchControllerに置き換え
- UINavigationBarのtranslucentを有効に
- Safe Area対応
今回は「Xcode 9への移行」と「Swift 4への移行」は割愛します。fastlane gymのあたりで詰まったところもあるのでもしかしたら別記事で書くかもしれません。
UISearchDisplayControllerをUISearchControllerに置き換え
今までUISearchDisplayController
を使っておりUISearchBar
をUITableView#tableHeaderView
に入れることで検索機能を使えるようにしていました。iOS 11ではUISearchBar
のサイズが変わっておりそのあたりに起因すると思われるレイアウトずれが数カ所で発生してしまいました。 UISearchDisplayController
はiOS 8から非推奨なのですが、いままで使い続けてしまっていました。 iOS 11ではUINavigationItem#searchController
にUISearchController
を設定する方法が推奨されていることもあり、UISearchController
への置き換えを行いました。
細かい変更内容については、検索画面の実装に依存する部分が大きいと思われるので今回は割愛します。
UINavigationBarのtranslucentを有効に
UINavigationBar#translucent
がappearanceで無効になっており、コンテンツがUINavigationBarの裏側に回り込まないようになっていました。これはiOS 7対応の時にUINavigationBarの裏側にコンテンツが隠れてしまう問題を回避するために行ったものと思います。
iOS 11ではスクロール位置がずれるなどの問題が発生してしまいました。原因はおそらくUIScrollView#contentInsetAdjustmentBehavior
と透過なしの動作がかち合ってしまったためと思います。 このまま開発を進めると、今後のiOSバージョンアップの時にもずれと戦う可能性を考えUINavigationBar#translucent
を有効にすることにしました。
基本的にはこれだけでよいはずなのですが、スクロール関連の自前計算をしている箇所が原因で期待通りに動いていませんでした。 具体的には下記のような項目です
- UITableViewのcontentInsetを自前計算してしまっているところがあった
- insetの上側が0であることを前提に自前レイアウトしているViewがあった(UINavigationBarの裏に表示されて常に見えなくなるViewがあった)
- contentOffsetを自前計算しているところでずれていた
このあたりを解消することで、ずれを解消できました。
Safe Area対応
記事の冒頭で載せたように、いくつかの画面でSafe Areaの外側に表示されることがふさわしくないものが表示されてしまっています。
Safe Areaに従って表示されない場所にはいくつかのパターンがありました
- UITableViewCell#contentView のサイズに従っていない
- UIToolBarではなく自前のバーを使っている
- View内のコンテンツを自分でレイアウトしている(UITableViewなど使っていない)
UITableViewCell#contentView のサイズに従っていない
メッセージが右側に食い込んでしまっていますが、これはUITableViewCell#contentView
のサイズではなくUITableViewCell
自身のサイズを元に子要素のレイアウトをしているためでした。iPhone X上ではUITableViewCell#contentView
はSafe Areaに基づいてレイアウトされるので本来であれば対応が必要ない部分ですが本来の実装方法に従っていないとこのようなところでしわ寄せが出てくることを知りました。
UITableViewCellのcontentView.bounds
を基準にレイアウトするようにすることでUITableViewCellのレイアウトはSafe Area対応ができました。
UIToolBarではなく自前のバーを使っている
UIToolBarのようなものを使っている場合、iOSがいい感じにSafe Area対応をしてくれます。しかし、自前のUIを下部に表示しているようなケースでは自分でSafe Areaを考慮したレイアウトにしなければなりません。
下記のようなことをしました
- バーを手動レイアウトからAuto Layoutに載せ替える & 下位置をSafe Area基準にする
- バーの下部分を白く塗りつぶすViewを追加
バーを手動レイアウトからAuto Layoutに載せ替える & 下位置をSafe Area基準にする
入力用のバーは手動でレイアウトしていました。キーボードが出現したときに上に移動したり、元の位置に戻したり、入力テキストの行数が増えたときにバー全体の高さを変えるのもすべて手動でした。
このように複数のUIが同時に変化するようなレイアウトはAuto Layoutで定義すると扱いやすいこともあり、この部分はAuto Layoutに変えました。扱っているアプリがiOS 9以降のみのサポートなのでNSLayoutAnchor
を使ってコードで制約を定義していきます。(まだこの部分はSwift移植できていないのでObjective-Cで書いています)
_messageBar.translatesAutoresizingMaskIntoConstraints = NO; [_messageBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:0].active = YES; [_messageBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:0].active = YES; _messageBar.heightConstraint = [_messageBar.heightAnchor constraintEqualToConstant:messageBarHeight]; _messageBar.heightConstraint.active = YES; if (@available(iOS 11.0, *)) { // Safe Areaの下位置とそろえる _messageBar.bottomConstraint = [_messageBar.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:0]; _messageBar.bottomConstraint.active = YES; } else { // Viewの下位置とそろえる _messageBar.bottomConstraint = [_messageBar.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor constant:0]; _messageBar.bottomConstraint.active = YES; } // 入力欄の行数に応じて messageBar.heightConstraint.constant の値を変化させる // キーボードの出現に応じて messageBar.bottomConstraint.constant の値を変化させる // UITableViewController#tableViewではなく、UIViewController#viewの先頭にtableViewをaddSubViewしている状態です self.tableView.translatesAutoresizingMaskIntoConstraints = NO; [self.tableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor constant:0].active = YES; [self.tableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor constant:0].active = YES; [self.tableView.topAnchor constraintEqualToAnchor:self.view.topAnchor constant:0].active = YES; [self.tableView.bottomAnchor constraintEqualToAnchor:_messageBar.topAnchor constant:0].active = YES; // tableViewの高さは、messageBarの上端位置に応じて自動的に調整される
iOS 11以降の場合はsafeAreaLayoutGuide
を基準にレイアウトすることでSafe Areaに応じたレイアウトにしています。iOS 10以前でこのプロパティを使うとエラーになるため@available
で処理を分けています。(最近のXcodeは処理を分けない場合に警告を出してくれるのでミスしにくくなっています)
後で変化するような部分はNSLayoutConstraint
をプロパティとして保持しておき、constantの値を変化させることでバーの高さを変えたりバーの位置を変更します。今まではframeを変更することで「位置の変更」と「高さの変更」を行っておりどちらを変えたいのかコード上から読み取りにくかったのですが、制約の値を変化させる方式にしたことでどちらを変更させるのか明確になるという利点もありました。
バーの下部分を白く塗りつぶすViewを追加
何もしない場合、上記のように下側のSafe Area部分はUIViewController#view
がそのまま見えてしまいます。Safe Areaにもスクロール可能なコンテンツを表示する場合など、それが望ましい場合もありますが、このアプリの場合は表示崩れのように見えてしまい望ましくないと判断しました。
そこで、バーの背面からViewの下端にかけて、バーの色と同じ色のViewを表示することにしました
(選択中のViewがバーです。その背面にViewを追加しました)
これにより、UIToolBarを表示したときのように、バーがViewの下端まで伸びたように見えます。
View内のコンテンツを自分でレイアウトしている(UITableViewなど使っていない)
自由にレイアウトしているところは、横方向のSafe Areaにほとんどの場面で食い込んでしまっていました。
初期状態でSafe Areaに重要なコンテンツの先頭部分が重なっているのがまずいですし、この部分は横方向にもスクロールできないです。このような箇所が幅広い画面で発生しており、対応箇所としてはこのタイプが一番多かった印象です。
箇所が多かったので、すべてをAuto Layoutにするようなことはせず、frame計算時にsafeAreaContentInset
を考慮するようにして対応しました。
この方法については下記の記事が詳しいので参照ください。
- (void)viewWillLayoutSubviews { [super viewWillLayoutSubviews]; CGRect bounds = self.view.bounds; if (@available(iOS 11.0, *)) { UIEdgeInsets safeAreaInsets = self.view.safeAreaInsets; bounds.origin.x += safeAreaInsets.left; bounds.size.width -= safeAreaInsets.left + safeAreaInsets.right; } // ここでboundsを基準に子要素のレイアウトを行う }
元々の処理はUIViewController#viewDidLoad
でレイアウトしていたのですが、このタイミングではsafeAreaInsets
が取得できないのでviewWillLayoutSubviews
で行うように変更しました。 また、これによりAutoresizingを行っていたのも不要になったのでフラグ設定も削除しました。
どうなったか
↑メッセージ入力バーがSafe Areaの上に表示されています
↑横向きの時もボタンがSafe Areaの内側にあります
↑縦スクロールしかしない要素が横のSafe Areaにはみ出さなくなりました
所感
今回のSafe Area対応は、アプリ内の多くのViewに対して変更をいれる機会ではないでしょうか。これを機にレイアウト処理をリファクタリングしたり、Auto Layoutに置き換え直すのもよいと思います。コードでのAuto Layoutに抵抗がある方もいるかもしれませんが、もしあなたのアプリが iOS 9 以上のみのサポートであればNSLayoutAnchor
でわかりやすく制約を定義することもできますしUIStackView
を使うこともできます。
弊社 direct は企業向けサービスなので、今すぐにiPhone Xを会社で導入するような企業は少ないかもしれません。そういう意味ではすぐに効くような顧客価値の提供にはなっていないという考え方もあるかもしれません。しかし、継続的に開発スピードを上げながら開発して行くには開発環境の更新は不可欠ではないでしょうか。古いバージョンのXcodeを使って開発し続けることは開発効率の点で最善ではないと考えます。そういう意味で、開発効率を上げることが最終的にはお客様への価値提供につながると考えています。