こんにちは、技術本部 Mobile Application グループの山名です。
来るべき iOS 16 に思いを馳せている今日この頃ですが、今回は Sansan iOS アプリにおける iOS 15 対応についてお話ししようと思います。
発生した問題
主に4つ発生したので、そちらを順に解説します。
NavigationBar の背景が透明になる

本題に入る前に、UINavigationBar の Appearance についてお話しします。
UINavigationBar はデバイスの向きとスクロールの有無によって適用される 4つの Appearance を持っています。
| 名前 | デバイスの向き | 適用されるタイミング |
|---|---|---|
| standardAppearance | Portrait | スクロール中 |
| scrollEdgeAppearance | Portrait | リストの上端 |
| compactAppearance | Landscape | スクロール中 |
| compactScrollEdgeAppearance | Landscape | リストの上端 |
適用されるタイミングを実際の動きでお見せすると以下のような感じです。NavigationBar の背景色が薄いグレーの時が standard と compact、濃いグレーの時が scrollEdge と compactScrollEdge です。


本題に戻ると、今回の問題を引き起こしているのは scrollEdgeAppearance と compactScrollEdgeAppearance です。scrollEdgeAppearace は nil だと standardAppearance を引き継ぎつつ背景色を透明にするのですが、iOS 15 から Large Title でないものにも適用されるようになったため、今回の問題が発生しています。また compactScrollEdgeAppearance は nil だと scrollEdgeApeparance を引き継ぐようになっているため、同様に背景が透明になってしまいます。
この問題の解決策は Developer Forum で Apple のエンジニアが解説しており、Sansan もそれを参考にしました。具体的には AppDelegate.application(_:didFinishLaunchingWithOptions:) で UINavigationBarAppearance を生成し、各種 Appearance に設定しています。
let appearance = UINavigationBarAppearance() appearance.configureWithOpaqueBackground() appearance.backgroundColor = .white UINavigationBar.appearance().standardAppearance = appearance let standardAppearance = UINavigationBar.appearance().standardAppearance UINavigationBar.appearance().scrollEdgeAppearance = standardAppearance UINavigationBar.appearance().compactAppearance = standardAppearance UINavigationBar.appearance().compactScrollEdgeAppearance = standardAppearance
Toolbar, TabBar の背景が透明になる

NavigationBar と同様に scrollEdge 系が悪さをしています。Toolbar は NavigationBar と同じく4つ、TabBar は compact 系がないため standard と scrollEdge の2つの Appearance を設定して修正しました。
(余談ですが、NavigationBar の scrollEdge 系はリストの上端の時に適用されますが、Toolbar と TabBar はリストの下端の時に適用されます)
Plain Style な TableViewHeader のスタイルが異なる

デフォルトのヘッダだと以下が異なるという中々謎な仕様変更が入っています...
- 上部にスペースができる
- 下部にセパレータが入る
- テキストのサイズが小さく、カラーが薄い
まず 1, 2 ですが、これらは iOS 15 から追加された sectionHeaderTopPadding にゼロを設定してやると治ります(スペースは分かるのですが、セパレータが消えるのがあまりにも直感的でなくて辛いです...)。Sansan では UIBarAppearance 系と同様に、 AppDelegate.application(_:didFinishLaunchingWithOptions:) で設定して修正しました。
UITableView.appearance().sectionHeaderTopPadding = .zero
次に 3 ですが、サイズは 17 → 15pt に、カラーは _plainTableHeaderFooterTextColor → secondaryLabelColor に変わっています。

サイズに関しては Dynamic Type のスタイルでいうと iOS 14 が Headline , iOS 15 が Subhead みたいなので、統一したい場合は tableView(_:willDisplayHeaderView:forSection:) でどちらかに合わせる形になります。ただし iOS 15 はサイズは Subhead ですが、ウエイトが Regular ではなく Semi-Bold になっているため、そこは追加で設定してあげる必要があります。
カラーに関しては _plainTableHeaderFooterTextColor が公開されていないので、 secondaryLabelColor に合わせるのが簡単でよさそうです。どうしても iOS 14 の方に合わせたいのであれば Light / Dark Appearance 時のカラーを調べて、 UITraitCollection.userInterfaceStyle で switch する形になります。
また Sansan iOS では出くわしませんでしたが、この記事を執筆する過程で Plain Style な TableViewFooter で 3 がそのまま発生すること、Grouped Style な TableView で 3 のカラーのみ _groupTableHeaderFooterTextColor から secondaryLabelColor に変化していることを確認しました...
加えて下記参考記事によると 1 のスペース問題は Grouped でも発生するらしく、iOS 15 対応の TableViewHeader / Footer 周りは中々厄介な感じになっています...(まあデフォルトを使うならそもそも OS 毎の差異に抗うなという感じではありますが)。
IB 上で配置した UIButton のスタイルが異なる

こちらはどちらかというと対応後に発覚した問題です。
UIButton では iOS 15 から UIButton.Configuration を用いて Plain, Gray, Tinted, Filled のスタイルを適用できるようになりました。一見すると Plain がこれまでのスタイルに相当しそうですが微妙に挙動が異なるため、iOS 14 以前と合わせたい時は Default を設定する必要があります。
しかし IB 上から配置できるのは新しく追加された4つのみとなっているため、毎度 Attributes Inspector で Default に変更しなければなりません。とはいえそれだとヒューマンエラーで抜け漏れたりする可能性があるので、暫定対応として CI 上でチェックして警告を出すようにしました。Sansan では Sider というサービスを使っており、goodcheck を用いて設定を書いているため、そこに追記しました。
- id: com.sansan.interfaceBuilder.UIButton.style pattern: - regexp: buttonConfiguration key="configuration" style="(plain|gray|tinted|filled)" message: "Default 以外の Style を設定すると iOS 15 未満かどうかで表示が変わるため指定しないでください"
おわりに
Sansan アプリにおける iOS 15 対応は以上となります。
今年は軽微な UI 崩ればかりだったので、去年に比べるとかなり楽でした。 この流れを iOS 16 も引き継いでくれることを祈りつつ、WWDC を待とうと思います。
ここまで読んでいただきありがとうございました。