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

私はiPhone/iPad両対応のアプリの開発を行っています。数日前に[iOS13対応を行った際にハマったこと](https://qiita.com/lavox/items/824add806ad2edb30c9d)を記事にしたのですが、その後さらにiPadOS 13に対応するに当たってもハマったことがありましたので、本稿で記事にしておきます。

とは言っても、iOS13対応は終わっていたので大体は問題なかったのですが、`UISplitViewController`関連で2つほどハマったことがありました。

## `UISplitViewController`の`preferredDisplayMode`の反映タイミングの違い

私のアプリは基本的には`UISplitViewController`を使ったMaster-Detail型で、Master側にリストを、Detail側でそのリスト項目の内容を表示する構造になっています。

初期起動時には、前回起動時に表示していたリスト項目の内容をDetail側に表示するために、Master側の`viewDidLoad()`内でどの項目を表示するかを決めて、その内容をDetail側で表示するような作りになっていました。これを成り立たせるためには、

- まずMaster側の`viewDidLoad()`が呼ばれる
- その後ににDetail側の`viewDidLoad()`が呼ばれる

となる必要があり、いろいろとした試行錯誤の結果以下のような実装にしていました(細かい部分は省略し、骨格のみ残してあります)。

```swift:
// SplitViewControllerのクラス
class SplitViewController: UISplitViewController {
    override func viewDidLoad() {
        print("SplitViewController - viewDidLoad start")
        super.viewDidLoad()
        // ここで一度allVisibleにして、配下のViewをロードさせる
        preferredDisplayMode = .allVisible
        print("SplitViewController - viewDidLoad end")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        print("SplitViewController - viewWillAppear start")
        super.viewWillAppear(animated)
        // ここでprimaryHiddenに戻して、Detail側の表示だけにする
        preferredDisplayMode = .primaryHidden
        print("SplitViewController - viewWillAppear end")
    }
}

// Master側のクラス
class MasterViewController: UITableViewController {
    override func viewDidLoad() {
        print("MasterViewController - viewDidLoad start")
        super.viewDidLoad()
        
        // 初期起動時にどのリスト項目を表示するか決めて、Detail側にセットする処理(詳細省略)

        print("MasterViewController - viewDidLoad end")
    }

    override func viewWillAppear(_ animated: Bool) {
        print("MasterViewController - viewWillAppear start")
        super.viewWillAppear(animated)
        print("MasterViewController - viewWillAppear end")
    }
}

// Detail側のクラス
class DetailViewController: UIViewController {
    override func viewDidLoad() {
        print("DetailViewController - viewDidLoad start")
        super.viewDidLoad()
        print("DetailViewController - viewDidLoad end")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        print("DetailViewController - viewWillAppear start")
        super.viewWillAppear(animated)
        print("DetailViewController - viewWillAppear end")
    }
}
```

これを実行すると、iOS 12以前のiPadでは以下のようになっていました。

```
--- iOS 12 での実行結果 ---
SplitViewController - viewDidLoad start
MasterViewController - viewDidLoad start
MasterViewController - viewDidLoad end
SplitViewController - viewDidLoad end
SplitViewController - viewWillAppear start
SplitViewController - viewWillAppear end
MasterViewController - viewWillAppear start
MasterViewController - viewWillAppear end
DetailViewController - viewDidLoad start
DetailViewController - viewDidLoad end
DetailViewController - viewWillAppear start
DetailViewController - viewWillAppear end
```

想定通りMaster側がまずロードされ、その後Detail側がロードされています。ところが、iPadOS 13で実行してみたところ以下のようになってしまいました。

```
--- iPadOS 13 での実行結果 ---
SplitViewController - viewDidLoad start
SplitViewController - viewDidLoad end
SplitViewController - viewWillAppear start
SplitViewController - viewWillAppear end
DetailViewController - viewDidLoad start
DetailViewController - viewDidLoad end
DetailViewController - viewWillAppear start
DetailViewController - viewWillAppear end
```

Master側がロードされなくなってしまいました。

詳細はよくわかりませんが、iOS 12では`SplitViewController`の`viewDidLoad()`内に書いた`preferredDisplayMode`の設定処理により、おそらく同期的にMaster側がロードされているのに対し、iPadOS 13ではMaster側のロードは留保され、留保しているうちに`SplitViewController`の`viewWillAppear()`に書いた`.primaryHidden`に設定する処理に上書かれてしまって、結果的に実行されなくなったというようなことのようです。

現行仕様では初期起動時はDetail側のみ表示していたのですが、Detailに対してMasterをオーバーレイ表示する形でも構わなかったので、以下のように対応しました。

```swift:
// SplitViewControllerのクラス
class SplitViewController: UISplitViewController {
    override func viewDidLoad() {
        print("SplitViewController - viewDidLoad start")
        super.viewDidLoad()
        // ここでoverLayに設定する
        preferredDisplayMode = .allVisible
        print("SplitViewController - viewDidLoad end")
    }
    
    override func viewWillAppear(_ animated: Bool) {
        print("SplitViewController - viewWillAppear start")
        super.viewWillAppear(animated)
        // ここで行っていたprimaryHiddenの処理はやめる
        print("SplitViewController - viewWillAppear end")
    }
}
```

実行結果は以下の通りです。

```
--- iPadOS 13 での実行結果 ---
SplitViewController - viewDidLoad start
SplitViewController - viewDidLoad end
SplitViewController - viewWillAppear start
SplitViewController - viewWillAppear end
MasterViewController - viewDidLoad start
MasterViewController - viewDidLoad end
MasterViewController - viewWillAppear start
MasterViewController - viewWillAppear end
DetailViewController - viewDidLoad start
DetailViewController - viewDidLoad end
DetailViewController - viewWillAppear start
DetailViewController - viewWillAppear end
```

```
--- iOS 12 での実行結果 ---
SplitViewController - viewDidLoad start
MasterViewController - viewDidLoad start
MasterViewController - viewDidLoad end
SplitViewController - viewDidLoad end
SplitViewController - viewWillAppear start
SplitViewController - viewWillAppear end
MasterViewController - viewWillAppear start
MasterViewController - viewWillAppear end
DetailViewController - viewDidLoad start
DetailViewController - viewDidLoad end
DetailViewController - viewWillAppear start
DetailViewController - viewWillAppear end
```

多少順番は違いますが、Master側がロードされてからDetail側がロードされるという順序は実現できました。初期表示仕様がDetail側表示からprimaryOverlayに変わってしまいましたが、それはそれで良かったのでOKです。
もし従来のDetail側表示を維持するのであれば、`SplitViewController`の`viewDidAppear()`で`primaryHidden`にするのもアリだったと思いますが、やめておきました。

*実際には、過去バージョンへの悪影響が怖かったので、日和って`if #available`で上記の変更はiOS13以降でのみ反映されるようにしました。*

## `UISplitViewController`のMasterを隠す際のアニメーション問題
元々、`UISplitViewController`のMaster側表示時はオーバーレイ表示にしており、Masterでリストを選択した時に、その内容をDetail側に表示しつつMaster側のリストは左にスライドして消えるという動作をしていました。

この左側にスライドして消えるアニメーションは以下のように実現していました。

```swift:
class MasterViewController: UITableViewController {
    // showDetailに対応するsegueの処理
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showDetail" {
            // Detail側に選択された項目を設定する処理(省略)
                
            // アニメーション処理
            UIView.animate(withDuration: 0.3) {
                self.splitViewController?.preferredDisplayMode = .primaryHidden
            }
        }
    }
}
```

このように`preferredDisplayMode`の設定処理を`UIView.animate()`に入れることで、リストは左側にスライドして消えていました。そう、iOS 12までは。

ところがiPadOS 13で実行してみると、リストは消えることは消えるのですが、左側にスライドするアニメーションはなく、一瞬でパッと消えるようになってしまいました。

~~ここまで書いておいて何ですが、この問題は現時点(2019/9/23)でまだ解決方法がわかっていません。iPadOS 13正式版で再確認後、動作が変わらないようであればAppleに問い合わせてみようかと思います。また、もし解決方法をご存知の方がいらっしゃいましたら教えていただけると幸いです。~~

<font color="Red">(2019/9/28追記)
Appleに問い合わせた結果、解決方法がわかりました。というか、元々のアニメーション方法があまり良くなかったようです。以下のやり方でアニメーションすることができます。切替用のボタンを押したことにする、という方法ですね。</font>

```swift:
class MasterViewController: UITableViewController {
    // showDetailに対応するsegueの処理
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        if segue.identifier == "showDetail" {
            // Detail側に選択された項目を設定する処理(省略)
                
            // アニメーション処理
            if let barButtonItem = splitViewController?.displayModeButtonItem {
                UIApplication.shared.sendAction(barButtonItem.action!, to: barButtonItem.target, from: nil, for: nil)
            }
        }
    }
}
```

以上、2件のiPadOS 13対応でハマったことでした。両方とも、`UISplitViewController`の`preferredDisplayMode`に関する挙動なので、この設定について動作が変わったようです。
Googleとかでいろいろと検索してみましたが、あまり情報が得られなかったので、参考のため記事にしてみました。