🌟

【iOS】Storyboardをコードに移植するノウハウ

2022/11/11に公開

Storyboardをコードに移植する、というのをやって、苦しんだので、そのときの泥臭いノウハウを書いていきます。

基本方針

Storyboardのコンポーネントをコードにしたいなら、1からコード書いた方がたぶん早いです。
ただ今回、僕が直面したのは、

  • サービス的にめちゃくちゃ重要
  • AutoLayoutの制約関係が複雑

というもので、1から書くとすると、リファクタリングの過程で仕様を落とすことになると思われました。
基本のUIデザインはFigmaにありましたが、条件によってヘッダー・フッターが追加されるとか、
コンポーネントが切り替わるとか、その辺りのデザイン仕様はStoryboardから読み取る他ない状態でした。

なので、基本的にはStoryboardからしっかり仕様を読み取って、愚直にSwiftのコードに落としていく、という方針でやっていきました。
(SwiftUIではない)

Storyboardの設定を読んで、それをコードに落とす。
言葉にするとただそれだけなんですが、まあ上手くいきませんでした。

以下、時系列的に書いていきます。

一つずつ移植しようとすると

まずStoryboard上で一番上のコンポーネントを一つだけ移植しよう、と進めました。

ところが。
そのコンポーネントのbottomが画面の一個下にあるコンポーネントのtopに制約がついている、というのが厄介でした。

一個下のコンポーネントはこの時点ではまだStoryboardに@IBOutlet接続されているので、
コード上のイニシャルタイミングだとまだ初期化されていなくて、nilが入っています。
なので、全移行が完了するまで、

@IBOutlet var view: UIView? {
    didSet {
        // ここで制約をつける
    }
}

みたいにして、制約をつける必要がありました。
ヘッダー・フッター関連の処理で、制約の優先度の切り替えもあって、それなりの複雑性がありました。

やってみたところ、コード化した部分の制約が壊れてしまい、直せなくなりました。
今振り返ると、別にdidSetで制約つけていたことが原因ではなかったんですが、このときは原因がわからず、1つずつ移植する方針を諦めました。

エイヤで全部移植する

もう仕方ないので、制約的に依存関係があったコンポーネントは全部エイヤで移植することにしました。
移植して、その後で調整……と思ったのですが、これも破綻しました。

まず@IBOutletをきちんと確認すること

制約が上手くいかないのでよく確認してみると、既存のコードに存在した@IBOutletが、実は接続されていないデッドコードだったことがわかりました。
デッドコードに対して制約をつけていたので、当然表示崩れるよね、というのが原因でした。

Storyboardを複製して残しておく

上手くいかない原因がわかったら、あとは瞬殺かと思われたのですが、それでも愚直にコードに落としていくのは時間かかりました。
単純に複雑な依存関係をちゃんと読み解けなかったのが大きいです。

とりあえずdidSetの中で制約つけるというのがキツかったので、最上位コンポーネントが移植できたタイミングで、Storyboardを消してしまうことにしました。
早めにStoryboardを消すことで、表示の不具合が純粋にコードに問題があると切り分けられるようになって、捗りました。

これは泥臭いけど、地味に大事なノウハウだと思ったんですが、このときStoryboardを複製して残しておくと便利でした。
修正前のStoryboardファイルをStoryboard2.xibみたいな名前で複製して、Git管理からは

git restore --staged Storyboard2.xib

ではずしておきます。
これでコミット履歴も汚れません。

地味にStoryboardの設定を見るためだけに修正前のブランチに切り替える、みたいなことをやるコストが高いので、この方法がいいと思います。
この方法を思いつく前はXcodeのInspectorのスクショ撮ったりしてたんで、ホントに涙ぐましいですね。

StoryboardのAutoLayout設定を読み解く

StoryboardのAutoLayout設定は、Inspectorで見ることになります。

これがまたわかりづらくて、例えばPriorityはEdit押さないと見れません。

またFirst ItemとSecond Itemは、制約をタップすると↓に飛ぶので、ここで見ることになります。

First ItemとSecond Itemってコードだとどっちがどっちだっけ?と思いましたが、普通にconstraint()する側がFirst Itemでいいみたいです。

NSLayoutConstraint.activate([
    firstItem.leadingAnchor.constraint(equalTo: secondItem.leadingAnchor)  
])

最初タップしないといけないことに気づかずに、今Inspectorで表示してる対象のコンポーネントがFirst Itemだと思いこんでコード書いてたので、痛い目を見ました。
タップすると元々見ていたコンポーネントから制約に飛ばされてしまうのですが、戻るボタンを押すと元のコンポーネントに戻ります。

これもまた泥臭い話ですが、最初これに気づかずに制約に飛ぶたびに戻れなくなって苦しんでいました。
リファクタリング的には、バカになって現行設定をリライトする、その後要不要は議論する、というのが大事だと思っているので、戻れなくなると困りました。

あとContent Hugging Priorityのデフォルトは250 or 251、Content Compression Resistance Priorityのデフォルトは750です。

Content Hugging Priorityのデフォルトが250か251どちらになるかは↓を参照してください。
https://qiita.com/shtnkgm/items/f0b189e4184fe6c90707#content-hugging-priorityとcontent-compression-resistance-priority

これ以外の数値が入っている場合、コード側も設定する必要があります。

view.setContentHuggingPriority(.defaultHigh, for: .horizontal) // 750
view.setContentHuggingPriority(.defaultHigh, for: .vertical)

まとめ

もし今後こういうStoryboardの解体作業があるとすれば、次の手順でやろうと思います。

  • まずStoryboardを複製して残しておく
  • @IBOutletで接続されているコンポーネントを優先してコード化する
  • コード化できたらStoryboardを消して、それでもビルドできる状態にする
  • Storyboard上だけに存在したコンポーネントをコード化する
  • 最後に制約を整える

(了)

Discussion