Xcode 11でビルドしたRetty iOSアプリの検索バーが突然反応しなくなった訳

f:id:rettydev:20191212214614p:plain

※この記事はRettyアドベントカレンダー16日目の記事です。

qiita.com

昨日は諏訪さんGraphQLでの認可に関する記事でした、こちらも併せてどうぞ。

はじめに

はじめまして、Rettyはらみチームの @imaizume です。

今年11月からRettyにJOINし主にiOSの開発をやっています。

好きな食べ物は沖縄料理、お酒はビール・泡盛・ワインが好きです、おすすめのお店をぜひ教えてください!

ではさっそく本日の内容に入っていきましょう!!

Retty iOSアプリのお店検索について

今回のテーマはRettyの重要な機能の一つであるお店検索です。

Rettyのお店検索では「場所」と「目的」の2つのクエリを入力することができ、これによってより最適なお店をユーザーさんにサジェストするようになっています。

各検索バー(UISearchBarTextField)には文字を自由入力できますが、入力途中で候補をサジェストする機能があり、その中から値を選んだ場合は選択値が検索バー内に表示されるようになっています。

場所については、Rettyが用意している地域カテゴリで検索するため、自由テキストではなく選択値のみがクエリとして有効になります。

また2つの検索バーの種別を直感的に認識してもらえるよう、左側にそれぞれマップとフォークのアイコンを表示させています。

このようにアイコンを表示させる領域は UISearchBarTextField.leftView をカスマイズして実装されています。

そして目的の検索バーに2つの値が入力されるか、または検索バー下のボタンをタップすることで、お店検索が実行され自動的に結果画面へと遷移します。

iOS 13で突然お店検索ができなくなる

これらはXcode 10でのビルド時は問題なく動作していました。

ところが、Xcode 11 (iOS 13 SDK)でビルドしたところ、両検索バーにおいて、テキストカーソルや入力中の値、サジェストの候補が一切表示されない不具合が発生しました。

また もう一方のテキストフィールドをタップしてもフォーカスが切り替わりません。

以下はその時のスクリーンショットです。

f:id:rettydev:20191212212928g:plain
入力できない不具合が起きたときの検索画面

検索後の画面でも、場所の候補選択ができていないためクエリが送られていない様子がわかります。

幸いにも、Xcode 11でビルドしたバージョンはリリース前だったため、事前の検証で発見し大事故を防ぐことができていました。

原因はiOS 13 SDKからのUISearchの仕様変更

そこでView Hierarchyを使って検索バー周辺のViewのサイズを確かめてみました。

すると... なんと、アイコンの表示領域である leftView が検索バーの幅いっぱいに広がっているではありませんか!!

f:id:rettydev:20191212203401p:plainf:id:rettydev:20191212203406p:plain
上: 正しい状態のleftView 下: 不具合時のleftView

正しい状態のleftViewと比較すると一目瞭然ですね!

でも、どうしてこんなことが起きてしまったのでしょうか。

どうやら調査の結果、このバグはiOS 13 SDKからの leftView の仕様変更に起因しているようでした。

iOS 12までは、 leftView のサイズを決めるのにはframeサイズを直接指定しかつその値は不変でした。

しかしiOS 13からは、サイズ決定時に leftView に代入されるViewの systemLayoutSizeFitting を呼び出す仕様に変わったようです。

systemLayoutSizeFitting とは、レシーバーとなるViewについている制約を考慮して計算されたサイズを返すメソッドです。

Rettyのアプリを確認したところ、leftView にはダイレクトにサイズを指定していました。

leftView?.frame = CGRect(x: 0, y: 0, width: 44, height: frame.size.height)

つまり

  • iOS 12まではサイズをframeで静的に指定する
  • iOS 13からはサイズをAuto Layoutで動的に設定する

というUIKitの挙動の違いによって、Rettyでは後者に正しく対応できておらず leftView の表示崩れが起きていたのでした。

両OSで動作させるにはframe指定とAuto Layout両方の指定が必要

そこで、frameサイズ指定している行を削除してAuto Layout指定に変えてみることに。

するとiOS 13では正しく動作したものの、今度はiOS 12以下では逆に動作しなくなってしまいました。

つまり逆も同様で、iOS 12ではframeサイズを指定しないとAuto Layoutでの制約通りには描画されないということのようでした。

ということで、最終的には iOS12以下向けに初期化時にframeによるサイズ指定を、iOS 13以降向けにAuto Layoutの制約を設定するコードを共存させるに至りました。

コードは以下のようになりました。

    class SearchTextField: UITextField {
    ...
        // leftViewに対する幅制約
        // iOS 13以降でサイズを決定するのに必要
        private var widthAnchorForLeftView: NSLayoutConstraint?
    
        override func awakeFromNib() {
            ...
            let width: CGFloat = 44.0
            widthAnchorForLeftView = leftView?.widthAnchor.constraint(equalToConstant: width)
            widthAnchorForLeftView?.isActive = true
        }

        func updateView() {
            let width: CGFloat = 44.0

            // iOS 12以下ではframeサイズで指定
            leftView?.frame = CGRect(origin: .zero, size: .init(width: width, height: frame.size.height))
            // iOS 13以上ではNSLayoutConstraintで指定
            widthAnchorForLeftView?.constant = width

            setNeedsLayout()
            layoutIfNeeded()
        }
        ...
    }

こうして無事Xcode 11でビルドしたアプリでもお店検索が動くようになりました!!

f:id:rettydev:20191212214015g:plain
無事にXcode 11でもお店検索が動作するように

今回の変更は地味に影響範囲が大きい割に公式の1次情報が見当たらなかったため、個人的にはもっとしっかり周知をしてほしいなぁと思った次第です(汗)

まとめ

  • UISearchTextFieldleftViewrightView のサイズ計算の挙動がiOS 13で変わった
  • iOS 12ではサイズを直接かつ静的に指定するがiOS 13からはAuto Layoutで動的に指定する
  • 両OSに対応して動かすためにはViewの初期化時に直接指定とAuto Laayoutの指定を両方行う

明日は二見さんによるデータ分析の記事になります、ぜひこちらもご覧ください!

そして、私と一緒にRettyで働きたいiOSエンジニアの方もぜひお気軽にご連絡ください!!

参考