iPhone Xでタブバーコントローラーとツールバーとマップビューが共存できない問題を無理矢理解決した

## 追記(2018-3-1)

iOS 11.2では、元記事のままですと逆にツールバーが操作不能になります。対応策を追記しました。

## はじめに

みなさん、自作アプリのiPhone X対応、お済みでしょうか。
私は済みました。が、ものすごい労力を強いられました。
以下の内容は、本来、「ガイドラインどおりにUIを設計し直してね」のひとことで済んでしまうものです。が、X以外では動いているものをわざわざUI再設計というのはもっとつらいよね、ということで、無理矢理対応しましたよ、という記述となります。

## TL;DR
* iPhone Xでは、タブバーコントローラーにナビゲーションコントローラーを組み込み、そのナビゲーションコントローラー付属のツールバーを表示させると、なぜかタブバーの高さぶん、上にずれてしまう
* 実は最新のガイドラインでは、タブバーとツールバーを共存させることは認められていない
* それはともかくとして、ナビゲーションコントローラー付属の代わりに、内部の子ビューコントローラーに直接ツールバーを配置すれば、問題は解決したように見える
* が、そうすると、その中に貼られているマップビューの「著作権情報」が、ツールバーの下に隠れてしまう
* これらを無理矢理解決させるには、子ビューコントローラーにツールバーを貼り、アニメーションさせつつ、ナビゲーションコントローラーのツールバーを透明化させて「表示」すればよい
* なお、この問題はiOS 11.2では<del>自然解決の可能性がある</del>→（追記）自然解決どころか、逆に操作不能になるため、除外の対応が必要です

## 未対処時の現象

この問題、（執筆時点で最新の）iOS 11.1.2においても、iPhone X実機でも、発生します。
[自作アプリ](https://itunes.apple.com/jp/app/id489857963)を、X対応前・対応後（といっても、Xに対応したサイズの起動画像を登録しただけ）のバイナリーでそれぞれ起動させると、以下のようになります。

![iphonex1.png](https://qiita-image-store.s3.amazonaws.com/0/98308/d8a1aba5-166a-f146-1b04-dc2ae7c3ccc5.png)

Xに対応するとこーんなに広い領域を使えるんですよ！ という話ではなく、どちらの画面も、明らかに、画面下部のツールバーが“自身の高さぶんだけ”浮いています！
誰がどう見てもバグにしか見えない画面。でもそのバグは私のせいじゃない！ Appleのせいだ！！ と当初は憤ってしまいました。

## 実はHIGで禁止

この問題については、英語圏ではとっくの昔から困っている方がたくさんおられるようでして、おなじみstack overflowに[ばっちりQ&Aがでておりました](https://stackoverflow.com/questions/46205703/ios-11-iphone-x-uinavigationbars-toolbar-spacing-incorrect-when-embedded-in)。

そして、Answerの中に思いっきり、

> ["Tab bars and toolbars never appear together in the same view."](https://developer.apple.com/ios/human-interface-guidelines/bars/toolbars/)

と、最新のHIGに記載されてるからまずいんでないの？ 的なコメントがついているではありませんか！
あー知らなかった。もうぐうの音も出ません。

## 場当たり的対処：ツールバーを自前で処理

しかし、最新のHIGを知ってからX発売までの間に、現行のUIを、他の機種も含めて刷新するのは私には不可能です。
そもそも、最新HIGになってからも審査に通ってきたアプリではあるので、やはりここは場当たり的対処でなんとかならないか。
というわけで、上記Q&AのAnswerに書かれていて、それにQuestionerが「そんなんわかっとるわい！」とキレているところの方策、「ツールバーを自前で処理」をしてみました。

やることは簡単で、

* Xかどうかの判定をする（[こちらを参考にさせていただきました](https://qiita.com/ShingoFukuyama/items/273ae989442f3d1d11b2)）
* Xなら、この画面を持つビューコントローラーに、UITtoolbarを手動で追加し、表示／非表示は自力でアニメーション
* Xでないなら、従前どおり、UINavigationControllerのtoolbarItemsを利用し、表示／非表示はsetToolbarHidden:animatedメソッドに任せる

です。

```objc:AppDelegate.h
#define IS_OS_11_X_TAB_BUG  (IS_OS_11_OR_LATER && UIDevice.currentDevice.systemVersion.floatValue < 11.19)
#define IS_IPHONE_X_TAB_BUG (IS_IPHONE_X && IS_OS_11_X_TAB_BUG)
```

```objc:MapViewController.m
- (void)loadView
{
    [super loadView];
    ////
    NSArray* toolbarItems = /*create*/;
    if (IS_IPHONE_X) {
        CGRect rect = self.view.bounds;
        rect.origin.y = rect.size.height - 44 - 62 - 20;
        rect.size.height = 44;
        self.myToolbar = [[UIToolbar alloc]  initWithFrame:rect];
        self.myToolbar.items = toolbarItems;
        self.myToolbar.hidden = YES;
        [self.view addSubview:self.myToolbar];
    } else {
        self.toolbarItems = toolbarItems;
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    if (!IS_IPHONE_X_TAB_BUG) {
        self.navigationController.toolbarHidden = NO;
    }
    ////
}

// 独自メソッド
- (void)showHideBars:(BOOL)show animated:(BOOL)animated
{
    [self.navigationController setNavigationBarHidden:!show animated:animated];
    if (IS_IPHONE_X_TAB_BUG) {
        if (show) {
            self.myToolbar.hidden = NO;
            if (animated) {
                CGRect frame = self.myToolbar.frame;
                frame.origin.y += frame.size.height;
                self.myToolbar.frame = frame;
                frame.origin.y -= frame.size.height;
                [UIView animateWithDuration:ANIMATION_DURATION animations:^{
                    self.myToolbar.frame = frame;
                }];
            }
        } else {
            if (animated) {
                CGRect origFrame = self.myToolbar.frame;
                CGRect frame = origFrame;
                frame.origin.y += frame.size.height;
                [UIView animateWithDuration:ANIMATION_DURATION animations:^{
                    self.myToolbar.frame = frame;
                } completion:^(BOOL finished) {
                    self.myToolbar.frame = origFrame;
                    self.myToolbar.hidden = YES;
                }];
            } else {
                self.myToolbar.hidden = YES;
            }
        }
    } else {
        [self.navigationController setToolbarHidden:!show animated:animated];
    }
```
（いろいろと汚いコードですが、Storyboardすら使っていない、古くからのつぎはぎコードのため、こうなっておりますm(__)m）

うん、これで大丈夫。

**…ではありません！！**

## マップビューの「著作権情報」が隠れてしまう

実際に上記の修正を施したコードを実機で動かしてみたところ、こうなりました…

![iphonex2.png](https://qiita-image-store.s3.amazonaws.com/0/98308/88301111-838f-f3b7-3a72-c6fe713f834b.png)

いっけん、これで解決、のようにも見えます。
しかし！ これを見た瞬間、私は、かつてGoogle Mapsを利用していたころのMkMapViewで、まさにこれと同じミテクレだったこと*だけ*を理由にアプリ審査でリジェクトされた苦い体験を、思い出しましたよ…

この2枚の画面、これは地図部分をシングルタップすることで、トグルで表示が切り替わるようになっているUXですが、左下の小さい小さい文字列「著作権情報」が、左のツールバー表示状態＝起動直後の状態で、みごとに隠れてしまっています。

そして、英語版では"Legal"と表示されるこのリンクつきラベル、これがデフォルトで表示されていない状態の場合、アプリ審査に落ちる可能性が生じてしまうのです。

## マップビュー内の「著作権情報」ビューの移動がiOS 11では不可能

しかしここで疑問です。この「著作権情報がツールバーで隠れる」問題、まさに前述のとおり、このアプリをはじめてリリースした太古の昔からずっとあったわけで、それを先人の知恵で解決していたはずではなかったのか？ と。。。

```objc:MapViewController.m
- (void)initializeLogo
{
    for(UIView *v in self.mapView.subviews) {
        if([v isKindOfClass:[UILabel class]]) {
            CGRect frame = v.frame;
            frame.origin.y -= 44;
            v.frame = frame;
            self.logoInitialized = YES;
            return;
        }
    }
}
```

これがそれを、少なくともiOS 10まででは実現していたコードです。単に、MkMapViewの子ビューを列挙して、UILabelはたぶん「著作権情報」だけだから、それをツールバーの高さぶん上にずらしてしまえ、という乱暴な何かですが、要は**これがiOS 11では動作していない**、ってことですよね。

いろいろ調べてみたのですが、どうやら、**iOS 11のMkMapViewの「著作権情報」ラベルは、Safe Areaに基づいて勝手にずれる**という仕様のようです。
上記コードを削っても、X以外の機種では（も）きちんとラベルは自動的にずれました。
ですので、これの対策として、safeAreaInsetsをいじるなどのいろいろ試行錯誤もしてみたのですが、うまくいきませんでした…

## 無理矢理解決：透明なツールバーを出しておく

あたまわるい私ですが、結局、うんうんうなった挙句、こんなトンデモハックが降臨してきました。

1. ナビゲーションバーのツールバーが表示されていれば、「著作権情報」がちゃんとずれる。
2. これはツールバーがhidden==NOであればよいってことだよね？
3. じゃ、ナビゲーションバーのツールバーを透明にしちゃえばいいんじゃね？？

```objc:MapViewController.m
- (void)loadView
{
    [super loadView];
    ////
    NSArray* toolbarItems = /*create*/;
    if (IS_IPHONE_X_TAB_BUG) {
        CGRect rect = self.view.bounds;
        rect.origin.y = rect.size.height - 44 - 62 - 20;
        rect.size.height = 44;
        self.myToolbar = [[UIToolbar alloc]  initWithFrame:rect];
        self.myToolbar.items = toolbarItems;
        self.myToolbar.hidden = YES;
        [self.view addSubview:self.myToolbar];
        // ナビゲーションコントローラー管理のツールバーを透明化する
        [self.navigationController.toolbar setBackgroundImage:[UIImage new]
                                           forToolbarPosition:UIBarPositionAny
                                                   barMetrics:UIBarMetricsDefault];
        [self.navigationController.toolbar setShadowImage:[UIImage new] forToolbarPosition:UIBarPositionAny];
    } else {
        self.toolbarItems = toolbarItems;
    }
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    // iPhone Xでも常時ナビゲーションコントローラーのツールバーを「表示」する
    self.navigationController.toolbarHidden = NO;
    ////
}
```

![iphonex4.gif](https://qiita-image-store.s3.amazonaws.com/0/98308/101defca-fa1f-c2b0-094e-4a6746606eb0.gif)

というわけで、なんとか解決しましたよ…
ただし、*「著作権情報」が妙に上の方にあるけどな！*
もちろん、これは、そのすぐ下に、ナビゲーションバーの透明なツールバーが「表示」されているからですね…

追記：iOS 11.2で、ツールバーがずれる問題が解消されると同時に、透明にしたタブバーのz-orderが自前で出したツールバーよりも前に出てきてしまったようで、透明なタブバーにタップを乗っ取られてしまう現象が発生しました。よって、11.2以降ではこのハックを行わないことに、明示的にしなければなりません。

## （蛇足）真の改善策は？

こうして、強引にもほどがある「解決」がいちおうできましたが、先にも書いたとおり、これは本来*HIG違反*です。
というわけで、タブバーかツールバーのどちらかをなくせ、っていうことになるんでしょうけど…

参考までに、同じ機能の[自作Androidアプリ](https://play.google.com/store/apps/details?id=com.wsf_lp.oritsubushi)の地図画面はこんな感じです。

![android.png](https://qiita-image-store.s3.amazonaws.com/0/98308/40ecf7ee-4d2e-35cf-8a89-953219f62325.png)

半透明にしたボタンやらエディットテキストやらを載せて、地図画面のシングルタップでこれらをフェードアウト／インアニメさせています。
そして各種画面への遷移は、タブバーではなく、ハンバーガータップでドロワーメニューから選択と。

もしiPhoneに持ち込むなら、タブバーはそのままで、ツールバーをAndroidと同じくフロートボタンにする、とかでしょうか。
でも、それ、いかにもAndroidっぽくて、iPhoneらしくないですよね…

もし、なにかよいアイディアがあれば、ぜひご教示いただけますと助かります！m(__)m