まだ Storyboard で消耗してるの?——Re:ゼロから始める視覚表現(ビジュアルリプリゼンテーション)

  • 209
    Like
  • 18
    Comment
More than 1 year has passed since last update.

煽り全開の dis りタイトルになってしまいましたが、まあそれくらい筆者が不満を抱いたってこととして受け止めていただければ幸いでございます。

ほとんどの iOS 開発の教材はおそらくこのように教えているでしょう。「Xcode 開いて、Main.storyboard をクリックして、blabla…」と。

そしてほとんど(筆者調べ)の開発者たちは、結局最後まで、Storyboard の効率的な使い方が見つからず、多少なりとも心のどこかで「F*ck」とぼやいているのではないかと。

というかそもそもそこまでたどり着く前に「Storyboard の使い方わかりましぇーん」で終わってしまう屍も数多くいるのではないかと。

アップルがどこまで「直接コードで画面作る」ことを嫌ってるかは知りませんが相当なもんでしょう。例えば未だに watchOS は UI は Storyboard でしか作れません。それを踏まえて今までコードで UI 組んできた筆者がまあ多分アップルに迎合しなくてはならない時期もいずれ来るだろうからという短絡かつ楽天な理由で一つだけ新しいプロジェクトを Storyboard 使ってみました。結論は一つ:

Storyboard is Motherf*cker

まあ今までずっとコードで UI 組んできたから Storyboard は自分にとって何もかもが合わないっていうのも一つの理由でしょうけど、本記事はそういった主観的な評価取り上げつつ、Storyboard を断罪したいと思います。

Storyboard のデメリット

いうほどビジュアルがわかりやすくない

いくら「デザインの場ですぐレイアウトが確認できる」と言ってもそもそもコンテンツが動的に変わっている時点で確認できるのはほんの一部だけだし、さらに UIAlertController などのようなまず Storyboard で画面遷移できないようなものにはまず確認できない。できるものであっても例えば UITableView のような非常に動的な動きに頼るものもほぼ確認不可能。結局いちいちアプリ起動して色々試すしかない。でもそれだけならまだいい。使っても使わなくても確認するだけだから。ところが Storyboard の最大の問題点は Storyboard 側で追加した要素は Storyboard 側でも設定できれば、ソースコード側でも設定できる。となると果たしてどっちで設定したのが正しいのか、というものは結局ソースコード追って、場合によっては実際起動してみてブレーカーなりなんなりで確認するしかない。UI 面を余計ややこしくしてるだけじゃないか。そもそも忘れるな。Storyboard は別に WYSIWYG ではない。

後からの仕様変更に対応(ほぼ)できない

例えば UIImageView 要素を Storyboard で追加したとしよう。追加したこの要素を色々設定もしました。動作確認して動きも OK。万歳。と思った次の瞬間、いきなりこんな要望を受けました。

ここでウェブページを表示したい

さてどうしよう。Storyboard で設置した UIImageView をそのまま UIWebView に直したい、と思ったらそれができない。すでに一度設置した UIImageView を取り外して、UIWebView を同じ場所に置き直して、前に UIImageView に対して行った設定を全て、もう一度やり直さなければならない。
こんな時ほど、このような言葉を口走りたいときはない。

このクソったれ

ソースコードとのリンクが Implicitly Unwrapped Optionals(暗黙な Forced Unwrapping)

別に IUO 自体が悪って意味ではない。null 安全に関して非常に細かく書かれたこの記事の通り、ものによっては IUO も必要です。こと UI に関して、IUO は一部の画面要素を必要になるまで作らないようにすることで UIViewControllerUIView オブジェクトの生成コストを下げるという大きな役割を買っています。

しかしこの IUO は UI 開発にそれなりに影響を与えているのも事実です。

どういうことかというと、UIViewControllerUIView のライフサイクルをきちんと理解しないと、少なくともこのライフサイクルのことを念頭に置かないと、痛い目にあうこともあります。例えばソースコードで画面遷移を作ったとします。遷移直前に、次の UIViewController にある要素を今の画面の結果をパラメーターとして何か渡してあげたい。こんな時に注意しないといけないことがあります。
渡したい相手の要素が、まだ生成されていない可能性がある
こんなとき、もはや直接コードで lazy var 書くか、もしくは Storyboard 自体が要素を lazy var として追加してあげればいいのに、と思うわけですよ。

ソースコードとの対応関係がわかりづらい

Storyboard を使うと、UI を直す場合、我々は常に Assistant Editor を片方 Storyboard にし、もう片方は対応する UIViewController クラスのソースファイルにしながら、どの要素がどの変数に対応しているのかだとか、を確認しながら Storyboard もしくはソースを修正しなければなりません。間違って対応関係を消しちゃったり、違うものに対応させたりしてもすぐには気づきにくい。

そして、Assistant Editor というのは当然 2 画面を使うわけだから、そもそもディスプレイがある程度の広さを確保できないと非常に辛い。12 インチの MacBook は開発機としての人権がないというのなら、じゃあせめて 13 インチの MacBook Pro は快適に開発できるようにしてほしい。しかし画面の狭い 13 インチの MacBook Pro の場合、Assistant Editor の 2 画面と一番左側の Navigator だけでも画面がキッツキツになるというのに、さらにその上右側の Utilities を開いてプロパティー設定やソースとの対応を確認するのは至難の技に近い。うん、やっぱりどっちかを諦めましょう。Assistant Editor 使うのなら Utilities は開くな。逆も然り。もし根本的にこの問題を解決したいのなら大人しく 27 インチの iMac を購入するか、外出先含めて外付けモニターを買おう。

ていうかそもそも ViewController に細かいビュー要素管理するのって変じゃない?

MVC であれ MVVM であれなんであれ、モダンな開発においてよく使われるアーキテクチャってのは「コントローラー(ビューモデル)」と「ビュー」の分離によって得られたユーザ制御と画面デザインの分離/専念は、ソースコードの再利用/メンテ性の向上に非常に重要な役割を果たしているっていうのに、Storyboard のやろうときたらいきなりコントローラーである ViewController に画面設計させやがって。

Auto Layout はクソ

Storyboard のクソさは Storyboard 自体だけの問題ではない。それ以上に大きな問題点がある。Auto Layout、お前のことを言ってるんだ。

Auto Layout はどれくらいクソかというと、「auto layout sucks」でググると 100 万件以上の結果がヒットします。比較としては微妙ですが「Xcode sucks」でググってもせいぜい 6 万件程度のヒット。Xcode はどうでもいいが Auto Layout くたばれ!と思う人の方がはるかに多いという結果になります。Auto Layout があまりにも不評すぎてアップルが自ら WWDC で「Mysteries of Auto Layout」というセッションを組んだ。二つも。そして結果は?Auto Layout に関して有益な情報何一つなしにただ単に UIStackView とかいう代替策を提示しただけ。Auto Layout の問題何一つ解決してねぇよクソが。まあ確かに今までクソ面倒だったレイアウト組が UIStackView を使うことで多少楽にはなるが。

ちなみにどうでもいいことだが「auto layout sucks」でググると筆者の友人が作った Auto Layout Sucks Substitution ALSLayouts というフレームワークも引っかかりました。

では具体的に何がダメなのか。

複雑なレイアウトにまず対応できない

これな。これ。
Auto Layoutは全て「制約」というもので制御しています。「このパーツは幅はそのパーツのと同じ」だとか、「このパーツとそのパーツの縦の空きスペースはこれくらい」だとか。こういうものです。そしてすべてのパーツが確実なフレームが決まるまで、アンビギュアスエラーとしてプログラマに警告します。さらに中には画面サイズによっては実現不可能なものもあるかもしれないので、制約にはさらに 0 から 1000 までの数値で決まる「プライオリティー」というプロパティーがあって、もし制約同士が衝突した場合はプライオリティーが喧嘩して偉い方が勝ちます。まあ今まで Auto Layout と数多く戦ってきたみなさんならおなじみですよね。

「制約」というものは結局のところビューを設定するときに設定してしまう静的なものですので、単純なレイアウトなら対応できる(簡単にとは言ってない)が複雑なもの、特に動的なものにはまず対応できない。そんな複雑なレイアウトを組みたい場合、結局のところ自分でソースコードを組むか、制約をコード上で修正したりしなければならない。

制約はほぼ全て「同一ヒエラルキー下」における「両者間(自分VS自分含む)」の設定

まあ一番厄介なのはこれですね。「制約」というものがクソ複雑になる元凶。さっきにも言ったが、単純なレイアウトなら制約で対応できるが、その対応が簡単とは言ってない。横並べの A、B、C 三つのビューをセンタリングするだけでも、やり方にもよりますがたとえば A と B の制約各々、B と C の制約各々、そして B とメイン画面の制約各々を色々設定しなければならない。まあ今ならこれは UIStackView で比較的に簡単に解決できるようにはなったが。

ただし問題はですね、「同一ヒエラルキー」なんですよ。上記の例、A と B との制約と B と C との制約は同一ヒエラルキーなのはわかるが、なぜ B と メイン画面との制約もが同一ヒエラルキーなのか。どう見てもこっちは明らかに一つ上の階層にあるべき制約だろ。この例なら確かに UIStackView 使えば解決できるが、単純に UIStackView だけで解決できないもの、例えば十字キーのような置き方をしたいなら?

しかも何でもかんでも「両者間」というのも非常にいただけない。いや比較だから両者が必要なのはわかるが、この場合の「両者」というのはまた「同一ヒエラルキー」の意味をしています。つまり、「A の大きさを B と同じにしよう」という問題の場合、これは文脈から察するに「B」は基準であって、A と B の大きさが一致しないなら A を変えて B と同じようにする、ということを意味しますが、「制約」を持ってこれを実現できない、少なくとも「直接的に」実現できない。どうしても実現したいならさらに B に違う制約、例えば絶対変わることがない「画面の大きさ」との制約を持たさなくてはならない。マンドクセ。

クソ多い、探しづらい、要素がちょっとでも膨らめばデバッグがほぼ不可能

その結果、「制約」というやらは Storyboard のあっちこっちに溢れており、しかもどれもこれもが同じヒエラルキーにあって、さらに名前もつけられないしデフォルトの名前も非常にクソ長い上リスト画面ではまずそのフルネームを表示することができない(表示するスペースがない)から、何かを探そうにも画面要素から探さないと見つかることが非常に難しい。かと言って別に画面要素から探しても探しやすさが格段に向上されるわけではない。多少探しやすくなっただけだ。

そして画面要素が増えると、制約の数はそれの幾何級数レベルかと思うレベルに増えて、レイアウト崩れたとき上記のヒエラルキーの問題も含めてデバッグするのはほぼ不可能になってきます。まあ UIStackView をうまく活用すれば確かにこの問題は多少軽減されるが根本的に無くなることはない。「すべての制約が同一ヒエラルキー」がなくならない限り。

ブラックボックス

デバッグしづらいもう一つの原因。結局のところ Auto Layout はオープンソースではなくブラックボックスになっていて、バグっててもそれは果たして自分の制約のつけかたが悪かったからかそもそも Auto Layout 自体がバグったからかわからない。どうにかしてバイパスするしかない。クソが。

多言語対応がまるで悪夢だったがなんとかならないことはなかった

アップルは果たして多言語対応はどこまで本気なのか、非常に気になります。ファーストパーティーだけを見てるとどこまでも本気のように見えます。Windows が未だに外字で文字化けするとか騒いでる中、macOS(昔の Mac OS X)はとっくに多言語化対応があたりまえになって、世界のどこで Mac を買っても設定一つ変えるだけで OS の言語が変わり、ファーストパーティーアプリなら文字化けなんてまずありえないしサードパーティーに関してもまあよっぽどのことがない限りそんなに文字化け案件に出くわすことはない。

そんなアップルだからこそ、開発ツールである Xcode も、多言語化対応はとっくに実装され、開発者たちが容易に多言語化対応できます。Localizable.strings で定義しておけば String.localized でローカライズされた文字列が得られますし、Storyboard も当たり前のように Localize ボタンがあって、それをクリックすると Xcode が自動で .strings (Language) ファイルを作ってくれて、必要な文字列を入れてくれます。

ところが結局のところ、Storyboard を使う以上、多言語対応は実は思うほど簡単ではないのです。なぜならば Storyboard 管理下の要素は全て、わけわからない文字列の ID で管理されているのです。

最初から全て設計しておかなければ後から追加するのが大変だったがなんとかならないことはなかった

もうこれね。Localize ボタンを押した時点で、Storyboard の中に何があるのか、何が翻訳必要か、などのようなものを自動で生成してくれるけど、そのあとは一切知らんぷり。例えばボタンを一つ追加した。しかし .strings (Language) ファイルは何一つ変わってくれません。ご自分でそのボタンのわけわからん ID(aLS-4w-UzA みたいなフォーマットになっているので本当に何が何だかわからん)を書いて、ボタンの各状態の表示テキストを表すプロパティ名を書いて、対応するテキストを書いて、さらにセミコロンももちろん忘れてはいけません、これでようやく追加の多言語対応ができました。一つや二つの言語だけだったらまだなんとかなるが対応する言語増えてくるといちいちこれらのテキストを追加してから対応テキストを入れないといけないのは本当辛い。しかももし動作確認して「あれ変わってないな」と思っても、果たしてそれはそのわけわからん ID を間違えたのか、それともプロパティ名を間違えたのか、自分で目を大きくして確認しないといけない。.strings ファイルを反映させるには一手間かかりますがこちらに記事に書いてある通り、一旦 .strings ファイルを .storyboard に変換させてもう一回 .strings ファイルに変換し直すと追加要素が反映されます。ただ自分のプロジェクトでこれやってみたら一部の要素の翻訳がベース言語に戻ってしまった現象が発生したが只今事実確認中です。

最初から全て設計しておいても後からメンテするのが大変だったがこれはなんとかなりそう

最初から全部設計し終わってから多言語対応してるから問題ないって?甘いな。もう忘れたか?あのわけわからん ID。

そう、仮に Storyboard が自動で生成してくれた多言語対応ファイルでも、結局のところ全ての要素は ID で示されています。おまけに一応コメントを作ってくれるが大した有用な情報はない。クラス名は教えてくれるけど。でもあのわけわからん ID は別に自分から検索できたりはしない。すなわちある程度時間が経つと、本当にもうこのオブジェクトはどれ?って忘れてしまって、さらに当時のベーステキストも忘れてしまったら、もう要素を全部一個ずつ ID を確認していかないとこれはなんなのかさっぱりわからなくなってしまう。

おまけに、まあ UIButton はこのようなことあまりなさそうですが、UIView とかなら、要素のクラスを変えたい場合、Storyboard なら古い要素を消して新しい要素を入れ直すしかないので、入れ直したら ID も変わってしまいます。これももちろん、自分で .strings ファイルを直すしかない。クソが。

ただこれもこちらの記事に書いてある通り、「Comment for Localizer」の欄を記述しておけば .strings ファイルにはちゃんとその通りコメントに追加してくれます。ふむふむこれなら確かに後でわからなくなることはなさそうです、きちんと記述することさえ忘れなければ。ごめんなさいアップル、やっぱ多言語対応に関しての熱意は人一倍持ってるようです。

結論:クソクソアンドクソ。産廃以下並みのゴミ。

「産廃以下」だと思ってたが少なくとも多言語対応はなんとかなりそうだからまあ産廃並みに昇格させてください。でもやはりアップルはさっさと CSS なりなんなり導入しろ。

でも現状はどうすればいいのか

まあぶっちゃけな話、やっぱり本当に単純なもの以外は Storyboard 使うな、と言いたいところです。特に複雑なレイアウトを組みたいとか、そんなに複雑でなくても画面要素がちょっとでも多ければ、地道にソースコードで画面を組むことをお勧めします。それができなくても、制約を使うな、UIStackView だけでどうにかしろ

UI は Storyboard ガン無視してソースコードで作る

やり方はいくつかあります。真面目なやり方なら筆者の昔の記事他の方の簡略版記事の書いてあるようなやり方もあれば、そんなの面倒だというのなら最近の筆者みたいにすでにテンプレにある(Single Window Application を選んだ場合の話だが)ViewController.swift ファイルをそのまま流用して viewDidAppear(_ animated: Bool) をオーバーライドして中に let controller = MyCustomViewController(); self.present(controller, animated: false, completion: nil) で Storyboard をバイパスすることもできます。

そして自分のカスタムビューをコントローラーに設置したい場合は、UIViewControllerloadView() メソッドをオーバーライドして、その中に self.view = myCustomView に設定してあげればいい。ちなみにこの場合 super.loadView() を呼び出しては行けない。また自分で loadView() を呼び出しても行けない。

レイアウトは UIViewlayoutSubviews() メソッド内で組む

まあ layoutSubviews() メソッドは、アップルの公式見解では制約で実現できない場合以外使うなのスタンス(まあ確かに layoutSubviews() は中の処理が複雑になると非常に重くなるから理解できなくはない)ですが、そんなのクソ喰らえだ。制約なんてクソ使えるか。もう制約なしで全部 layoutSubviews() でレイアウト組んでやる。

とはいえ確かにこれは書き方によっては非常に重くなることもあるので、書く際にははいくつか注意は必要なのは間違い無いでしょう。たとえばなるべく複雑な計算するよりも単純な場合わけで定数を渡したり、自分で自分のフレームを決めたりせずあくまで自分のサブビューだけのフレームを決めたり。ただですね、全てのレイアウト処理を一箇所でまとめることによって得られるデバッグとメンテのしやすさは非常に大きいです。こんなの覚えてしまったらもう Auto Layout とかいう外道に触れたくなくなります。

ちなみに同じく公式ドキュメントに書いてある通り、このメソッドはあくまでオーバーライドするだけであって、直接呼びたしたりはしてはいけない。レイアウトし直す場合は setNeedsLayout()layoutIfNeeded() を呼ぼう。

多言語対応は Localizable.strings で

どういうことかというと、ボタンのタイトルテキストなどはソースコード上そのまま設定せず、"MainViewController_OK_Button_Normal".localized などのように設定します。そして Localizable.strings の中に "MainViewController_OK_Button_Normal" = "Button Title"; のように設定します。もちろん右側は言語に合わせて適切なテキストに変更しますが。

こうすることによって、テキストとソースコードの分離ができる上、どのテキストはどこにあるのかといった情報も組み込めるので、Storyboard のクソ役立たん aLS-4w-UzA のような無意味な ID よりははるかにメンテ性が高い。もちろんソースコード上で要素のクラスを変更しても中身さえ変わらなければここを書き直すこともない。らくちん。

Oh please let storyboard die.

Nobody's waiting for his touch.