アプリのiOS13対応でハマったこと

私はiOSのアプリの開発を行っているのですが、そのままではiOS13で動作しませんでした。iOS13対応で必要だったこと、ハマったことについて書いておきたいと思います。

## Xcode11でストーリーボードとソースコードの両表示方法がわからない
*(2019/9/21追記)*
まず最初につまずいたのはここでした。XcodeのストーリーボードでUIコンポーネントをソースコードに対応づけるために隣り合わせて表示させるアレです。Xcode10までは右上の方に◯印2つが重なり合ったようなアイコンを押していたのですが、Xcode11ではそれがなくなってしまい、どうやって表示するのかわからなくなりました。

「アレ」はアシスタントエディタというのが正式名称のようで、ストーリーボードが表示された状態で、EditorメニューのAssistantを選ぶことで無事に表示されました。

## `UISearchBar`内のテキストフィールドの公式サポート
従来、UISearchBar内のテキストフィールドへのアクセスは公式にはサポートしておらず、非公式の`searchBar.value(forKey: "_searchField") as? UITextField`等として無理やり取得して使っていました。

iOS13ではこのテキストフィールドへのアクセスが公式APIとして提供され、

```swift
let searchField = searchBar.searchTextField
```

で取得できるようになりました。しかもこのクラスは`UISearchTextField`というクラスで、token表示機能（iOS標準のメールアプリの検索機能とかで、カードっぽい表示になるやつです）をサポートしています。

これだけなら既存アプリへの影響はなさそうですが、これに伴って2つハマったことがありました。

### `_searchField`のアクセス禁止問題
元々非公式だったので問題というのも何ですが、テキストフィールドへのアクセスの公式化に伴って、アプリからの`_searchField`へのアクセスが禁止されました。このため、この方法でテキストフィールドを取得していた場合には公式APIに変更が必要になりました。

### `Any?`からプロトコルへのキャスト問題
この問題は直接的にiOS13と関係あるわけではないのですが、自分のアプリでは元々`_searchField`で取得したテキストフィールドを使って、無理やりtoken表示っぽいことを行っていました。
今回、token機能が公式にサポートされたので、ありがたく使ってみることにしました。

`UISearchTextField`にトークン表示を追加する場合は、`UISearchToken`というクラスを使うことになります。この時、表示上のアイコン・文字列もそうですが、何を検索するのか（例えば日時を検索するとか）も合わせて覚えておく必要があります。そのために、`UISearchToken`には`representedObject`という`Any?`型のプロパティがあり、ここにその情報を詰め込んでおくことで後から取り出して検索することが可能になります。

で、検索するキー項目が複数ある場合に（日時と名前とか）、自然と`representedObject`に詰め込むクラスは何かの共通プロトコル(ここでは仮に`SearchKey`とします)を実装したクラスになると思います。
以下のような状況を想定します。

```swift
// 検索キー用の共通プロトコル
protocol SearchKey {
}
// 日付検索用のキー
class DateKey : SearchKey {
}
// 名前検索用のキー
class NameKey : SearchKey {
}
```

`representedObject`に`DateKey`や`NameKey`のオブジェクトを入れて取り出すときに、

```swift
let searchKey = token.representedObject as? SearchKey
```

のように取り出したくなりますが、何とこの方法では`nil`になってしまい取り出せません。

https://stackoverflow.com/questions/42033735/failing-cast-in-swift-from-any-to-protocol

上記URLにその情報があり、無事解決したのですが、Swiftでは`Any?`型をプロトコル型に直接キャストすることができないようです。一度`AnyObject`型にすると良いという解決策があるとのことだったので、以下のようにして回避しました。

```swift
let searchKey = token.representedObject as AnyObject as? SearchKey
```

### 検索バーのキャンセルボタンにまつわる問題
(2019/12/19に追記しました。iOS13対応直後は気付きませんでしたが、この問題が発生していたようです)
私のアプリでは検索した結果の一覧画面に編集ボタンがあり、編集押下時には元々検索入力エリアを非活性にするとともにキャンセルボタンを消していたので、iOS13対応では以下のようにコードを書き換えました。

```swift
let searchBar = parentVC.searchController.searchBar
searchBar.setShowsCancelButton(false, animated: true)
searchBar.searchTextField.isEnabled = false
```

ところが、`setShowsCancelButton`を実行してもキャンセルボタンは消えず、編集のキャンセルボタンで編集モードを抜けた後（この際、上記と逆のコードを実行しています）、検索のキャンセルボタンで元の一覧画面に戻ると、検索のキャンセルボタンが残るという事象が発生しました。

この問題の解決方法は現時点でわかっていませんが、上記の`setShowsCancelButton`を行わないと発生しなくなることがわかりました。`setShowsCancelButton`はキャンセルボタンを消すために呼んでいるはずなのですが、消えない上に上記のような問題を引き起こすだけなので、暫定処置としてスキップするようにしました。
キャンセルボタンは消えないままですが、一覧画面に戻った時にボタンが残る問題は回避できました。

## `UITableViewCell`のラベルの翻訳失敗問題
<font color="red">この問題はXcode11.3では解消しているようです(2019/12/19確認。11.2で直っていた可能性あり)</font>
私のアプリではstoryboardを使用して画面を開発しています。英語・日本語両対応させているので、まずは英語で画面を作成し、Main.stringsで該当するObjectIDに対して日本語を設定するというやり方です。

ところが、Xcode11 GM Seedで開発していたところ、いつの間にか翻訳されないようになってしまいました。私のアプリの場合は、staticな`UITableView`に配置している`UITableViewCell`のタイトルが一律翻訳されなくなりました。それ以外のコンポーネントは普通に翻訳されているので、おそらくXcodeのバグではないかと思っているのですが、うまい解決方法がなく、以下の方法で暫定回避しました。

1. 翻訳が失敗している`UILabel`の`ObjectID`の値を`Accessibility`の`Identifier`(IB上で、`ObjectID`のちょっと下にあるやつです)に手動でコピー
2. その`UITableView`のDataSourceクラスに対して、以下を追加実装

```swift
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = super.tableView(tableView, cellForRowAt: indexPath)
    if let label = cell.textLabel, let id = label.accessibilityIdentifier, id.count > 0 {
        let key = id + ".text"
        let localizedString = NSLocalizedString(key, tableName: "Main", comment: "")
        if key != localizedString {
            label.text = localizedString
        }
    }
    return cell
}
```

要は、`Accessibility Identifier`が存在して、Main.stringsに対応する翻訳が存在する場合に、テキストを置き換えるというのを自前で実装しています。
これで無事翻訳されるようになりました。

注意点としては、手動で`ObjectID`をコピーする際に、間違えないようにすることでしょうか。間違って他のIDを貼り付けてしまうと、別の訳語が表示されてみっともないことになります。

なお、Stack overflowで同じ問題を見つけたので、上記の回避策を投稿してみました。
https://stackoverflow.com/questions/57905965/xcode-11-ios-13-localization-issue

## `UISearchDisplayController`廃止によるアプリクラッシュ問題
一通り対応を終えて、AppStoreにアップロードしてTestFlightで動作確認しようとしたところ、起動時にアプリがクラッシュする現象が発生しました。シミュレータでのデバッグや、ローカルでの実機デバッグではうまく動いていたので調査は難航しましたが、

```
3   UIKitCore                     	0x1a3227148 UISearchDisplayControllerNoLongerSupported + 247
```

このクラッシュログを手がかりに、以下の情報にたどり着きました。
https://github.com/mapbox/mapbox-gl-native/issues/15559

かなり前から非推奨になっていた`UISearchDisplayController`がiOS13で完全に廃止されたため、使っているとクラッシュするという問題のようです。

問題は、こんなクラスを使用している覚えがなく、ソース全体を`UISearchDisplayController`で検索しても見つからなかったので、何故こんなエラーが出るのか途方に暮れていましたが、実はありました。

![スクリーンショット 2019-09-17 22.37.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202945/3c4a9ef3-da6c-fd3c-a813-4fb727c7e7d8.png)

どうやら、検索機能を開発する際に試行錯誤して追加した実際には使われていないものが残っていたようです。この記述は`UISearchDisplayController`で検索しても見つからず、`searchDisplayController`で検索する必要があるので注意しましょう。

これを削除したら解決しました。

## メインスレッド以外からのUIコンポーネントアクセスチェックの厳格化
元々画面描画関連の処理はメインスレッドから行わないといけませんでしたが、直接的に画面描画を伴う処理ではなくてもUIコンポーネントに対するアクセスのチェックが厳しくなったようです。私のアプリの場合は、`UIViewController`の`splitViewController`や`navigationController`に別スレッドからアクセスしている処理があり、チェックに引っかかりました。
単にその`ViewController`クラスで保持している自前のプロパティにアクセスしたいだけだったので、`splitViewController`や`navigationController`を経由せずに直接目的のViewControllerにアクセスするようにして回避しました。

## Xcode11 GM Seed2でiOS13のシミュレータが動かない
*(2019/9/21追記)*
Xcode11のGM Seed2にして以降、iOS13のシミュレータを起動しようとすると`The iOS 13.0 simulator runtime is not available.`というメッセージが出て起動しなくなってしまいました。詳細表示すると`runtime path not found`と出ていました。しばらく理由がわからなかったのですが、いろいろ検索した結果、以下の記事が当たりでした。
https://stackoverflow.com/questions/53261256/xcode-10-simulator-runtime-is-not-available-runtime-profile-not-found-error

```bash
$ sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService
```
これでシミュレータサービスを再起動することで問題が解消しました。

以上、iOS13対応している方の何かの役に立てば幸いです。