クックパッド開発者ブログスマートフォン用の表示で見るクックパッド開発者ブログ2015-11-04iOSアプリケーションでコードベースのレイアウトを積極利用する海外事業向けのiOSアプリケーション開発を担当している西山(@yuseinishiyama)です。クックパッドは現在、海外複数カ国に向けてサービスを展開しています。Builderと呼ばれる、リッチなGUIを持ったデザインツールが付属しており、これを用いて画面のレイアウトを構成することが主流となっています。弊社ブログでも、iOS開発でstoryboardとxibをうまく使い分けるプラクティス等の記事で、GUIベースのレイアウトについて触れています。Builderを用いずに、レイアウトの大半をコードで記述しています。今回は、コードベースのレイアウトを実装していく中で得た知見を、以下の3つの部分に分けて共有したいと思います。Builderを用いたレイアウトとコードベースのレイアウトの比較コードベースのレイアウトのデメリットへの対処コードベースのレイアウトの実装ちなみに、当該プロジェクトがサポートしているiOSバージョンはiOS7以上です。そのため、iOS8以降でしかサポートされない機能については触れません。既存のObjective-CアプリケーションをSwiftで書き換えた話でも触れましたが、殆どのコードをSwiftで記述しているため、今回掲載するサンプルコードも全てSwiftとしました。Builderを用いたレイアウトとコードベースのレイアウトとを比較し、それぞれのメリットについて説明します。Builderを利用してレイアウトを組むことには以下のメリットがあります。画面遷移をグラフィカルに確認できるレイアウトの結果がひと目で分かるプログラミング経験の無い人であっても容易にパラメータを調整できる本項では、それぞれについて説明します。画面遷移をグラフィカルに確認できるStoryboard上には複数のViewControllerを配置することが可能です。そのため、複数の画面間の遷移をコードを読まずとも把握することができます。レイアウトの結果がひと目で分かるGUIを利用してレイアウトを行うため、実装しながら実行時のレイアウトを確認することができます。そのため、レイアウトを変更する度に、アプリケーションをBuild&Runして結果を確認する必要がありません。プログラミング経験の無い人であっても容易にパラメータを調整できるマージンを変更したり、背景色を変更したりする際に、ソースコードを変更する必要がありません。GUI上のパラメータを調整するだけでこれらの値を変更できるので、プログラミング経験の無いデザイナー、ディレクターの人でもデザインを変更することが可能です。コードベースのレイアウトのメリット一方で、コードベースのレイアウトには以下のメリットがあります。ViewControllerを適切に初期化できるコードの変更をレビューしやすい定数を用いて、一貫したデザインポリシーを適用できる要素の変更に柔軟に対応できる要素の設定に関するコードが分散しない本項では、それぞれについて説明します。ViewControllerを適切に初期化できるStoryboardからViewControllerを初期化して、そのViewControllerへと画面遷移するケースを考えてみましょう。(省略)}このコードには2つの問題点があります。1つは遷移先のViewControllerのプロパティrecipeがImplicitlyUnwrappedOptional<Wrapped>として宣言されている点にあります。上記のコードにおいて、recipeは必須の(常に値が存在することが期待されている)プロパティです。StoryboardからViewControllerをインスタンス化する場合、インスタンス化の時点では、ViewControllerの持つプロパティを初期化することができません。そのため、必須のプロパティであっても、インスタンス化した後に代入する必要があります。Recipe()つまり、ここでは以下の2つを満たす必要があります。recipeが一時的にnilになることを許容するrecipeが必須のプロパティであることを表現するそして、この両方を満たすには、プロパティをImplicitlyUnwrappedOptional<Wrapped>として宣言する必要があります。これは、IBOutletとして、Storyboard上の要素をコードと接続する際に自動生成されるコードに関しても同様です。UILabel!しかし、ImplicitlyUnwrappedOptional<Wrapped>を使う以上、潜在的にはクラッシュする可能性があります。recipeプロパティに値を代入せずに画面遷移してしまい、その後、このプロパティにアクセスすればクラッシュしてしまいます。true)もう1つの問題点は、as!を用いた強制的なキャストが発生している点です。これは、instantiateViewControllerWithIdentifier(_:)の戻り値の型がUIViewControllerなので、具体的な型へとキャストする必要があるためです。このコードも潜在的にはクラッシュの可能性があります。ViewControllerのIDの指定を間違った場合などが典型的なクラッシュの例となるでしょう。DetailViewControllerFromStoryboardSwiftは静的型付けやnull許容性のコントロールによって、実行時のエラーを減らし、コードの誤りができるだけコンパイル時に検出されることを意図している言語です。一方で、Storyboardを用いたViewControllerのインスタンス化は「プログラマが」気をつけないとクラッシュする可能性があるという点で、Swiftの安全性を損ねていると言えます。Builderが元々はObjective-Cという言語の動的な特性を最大限に活かしたツールであるということを考えると、当たり前かもしれません。Builderを用いずに、コードだけでViewControllerを初期化する場合のコードは次のようになります。(省略)}通常のイニシャライザを利用できるので、プロパティrecipeが必須のプロパティでかつ再代入不可能であるということを強制することができています。nil)}ちなみに、init(nibName:bundle:)はUIViewControllerの指定イニシャライザです。コードの変更をレビューしやすいStoryboardは単なるXMLですが、その構造が非常に複雑であるため、レイアウトの変更があった際に、差分を見ただけでその意図を汲むことは容易ではありません。意図的に編集していない箇所が勝手に更新されることがある、という点も問題です。次のスクリーンショットは、ある制約の値をStoryboard上で12から10に変更した場合の差分です。一方で、コードであれば変更箇所のチェックは容易です。次のスクリーンショットは、ある制約の値をコード上で12から10に変更した場合の差分です。「userNameLabelというラベルの末尾のスペースが12から10に変更された」ということが明らかに見てとれます。ちなみに、コード上での制約の設定を簡潔に行うために、PureLayoutというライブラリを使用しています。このライブラリに関しては後述します。Builderを用いてレイアウトを構成する場合、レイアウト間で共通の値を設定することができません。コードベースのレイアウトであれば、定数を定義しておき、それを複数のレイアウト間で共有することが可能です。(省略)}このように定数を定義しておけば、ポリシーが変更された場合もその定数の値を変更するだけで変更を全体に適用できます。「アプリ全体で余白感がもっと欲しい」となれば、この定数の値を大きくすれば良いでしょう。要素の変更に柔軟に対応できるレイアウトを実装している途中で、View要素の型を変更したくなるケースがあるかもしれません。当初はUIButtonで実装していたものを、より複雑なレイアウトになっため、UIViewとサブビューで置き換える場合などがそれにあたります。Builderでレイアウトを行っている場合、このようなケースでは、一度元の要素を削除してから、新たな要素を追加し、もう一度制約を設定し直すということが求められます。コードベースのレイアウトであれば、単純に型を変えるだけで対応できます。Builderを用いれば、GUI上でView要素のプロパティ、例えば背景色やフォントなどを設定できることは既に説明しました。実際のアプリケーションでは、これらの値が動的になることも頻繁にあります。Builderで設定され、あるプロパティはコードで設定されている」というような状況が発生します。なってくると、最終的な見た目がどこで決定するのか分からない状況が生まれてしまいます。Builderで設定した不必要な値がコードで上書きされていたりすると、混乱を招きます。コードベースでレイアウトしていれば、ビューに対して適用される全ての設定をコードから把握できます。ビューの設定に関するコードでViewControllerが汚染されることを嫌う傾向もありますが、Swiftではプロパティの初期化に式が利用できるので、以下のようにView要素の初期化に関するコードを一箇所にまとめれば、ViewControllerの可読性を下げることもありません。次のようにクロージャを利用してプロパティを初期化すれば、ありがちなviewDidLoad()の肥大化を防ぐことができます。Builderを利用したレイアウトには次のようなメリットがあることを説明しました。画面遷移をグラフィカルに確認できるレイアウトの結果がひと目で分かるプログラミング経験の無い人であっても容易にパラメータを調整できる裏を返せば、コードベースのレイアウトには次のようなデメリットがあることが分かります。画面遷移をグラフィカルに確認できないレイアウトの結果がひと目で分からないプログラミング経験の無い人が容易にパラメータを調整できない本説では、これらのデメリットにどのように対処しているか、ということについて説明します。画面遷移をグラフィカルに確認できないことへの対処ある程度の規模のプロジェクトになると、その画面遷移は大変複雑になります。同じ画面に、複数の箇所から遷移したり、画面遷移時にナビゲーションコントローラのスタックを操作したり、といった状況になることも珍しくありません。した、複雑な画面遷移になってくると、Storyboardを使ってグラフィカルに表現したとしても、あまり視認性が高くありません。複雑になり、管理が難しくなる可能性さえあります。担当しているプロジェクトも、すでに画面遷移が複雑になりつつあります。大規模になる可能性があるプロジェクトなので、Storyboardを利用した画面遷移の管理にはあまり期待していません。レイアウトの結果がひと目でわからないことへの対処レイアウトの結果が実行するまで分からないことは、コードベースのレイアウトの最大の欠点と言わざるを得ません。ところで、レイアウトの結果が実行するまで分からない、という問題は以下2つの問題へと分解できます。当該プロジェクトでは、Zeplinというツールを用いてレイアウトを管理しているので、前者に関しては問題になりません。Zeplinを確認すれば、レイアウトの全体像が確認できるからです。以下は、Zeplin上で表示されているレイアウトの例です。マージン、ラベルのフォント等のプロパティが表示されていることが分かります。基本的にレイアウトに関する作業は、これを正解の状態として、それを実現する作業になります(誤解を招かないために補足しておくと、これはデザインがトップダウンで決定されるという意味ではありません。エンジニアもデザインの段階から議論に参加しますが、最終的に決定されたレイアウトが、Zeplin上に展開されるという意味です)。後者に関しては、確かに、コードを見ただけで最終的な結果を想像するのは容易ではないので、デバッグする際は何度もBuild&Runを繰り返すことになり、余計な時間を消費します。Xcode7+Swift2になってからコンパイル時間が大きく改善されたので、こうしたトライアンドエラーは現実的な時間内に行えるようになりました。AutoLayoutをラップしたサードパーティーライブラリを使用すれば、幾分か直感的にコードを記述することが可能なので、慣れてくれば、実行して確認しなくとも正しいレイアウトのコードを書くことができるようになります。プログラミング経験の無い人が容易にパラメータを調整できないことへの対処プログラミング経験の無い人によるデザイン調整が可能である、ということは、デザイナーが直接レイアウトを修正するようなプロジェクトでは当然重要でしょう。GUIによるレイアウトを採用する理由で、最も良く耳にするのもこれかもしれません。一方で、私が担当しているプロジェクトでは、直接デザイナーがデザインを修正するようなケースはほとんどありません。デザインから実装までのフローが次のようになっているためです(デザインの変更がある場合も、基本的には同様のフローを繰り返します)。Sketch、InVision等のツールでプロトタイプを作成(デザイナー)エンジニアがプロトタイプを確認した上でデザイナーと議論し、デザインをFixする(デザイナー、エンジニア)Zeplin上でレイアウトを確認できるようにする(デザイナー)Zeplinを確認し実装する(エンジニア)(注:カッコ内はその作業を担当する職種)また、デザイナーはデザインを行うと同時に、「デザイン設計」も行っています。ここでの「デザイン設計」とは、デザインの意図を元に、統一したデザインポリシーを定義することを意味します。「投稿に関連するアクションに紐付いたボタンの色」、「List形式のビューの親ビューに対するInset」といったように、コンテキストに対して決められた値が存在します。このことから、局所的なデザインの微調整が入ることはそれほど多くありません。した要因から、当該プロジェクトでは、プログラミング経験の無い人がデザインの修正を直接行えないことは問題になっていません。本節ではコードベースのレイアウトの実装方法について説明します。サードパーティライブラリを使用してレイアウトのコードを簡潔にする標準の方法を用いて、コードでビューの制約を記述するのは簡単とは言えず、コードも冗長になりがちです。これを解消するためのサードパーティライブラリが存在しています。以下のライブラリが有名でしょう。SnapKitPureLayout私が担当しているプロジェクトでは、PureLayoutを採用しました。PureLayoutを利用すれば、制約に関するコードをかなり簡潔に記述することができます。Constant.Inset.M)レイアウトに関するプロパティやメソッド本項では、コードベースでのレイアウトを実現する上で、重要なプロパティやメソッドを紹介します。translatesAutoresizingMaskIntoConstraints──AutoLayoutでAutoresizingMaskを表現するこのプロパティがtrueになっていると、AutoresizingMaskと同様の挙動を実現するための制約が自動的に設定されます。AutoLayoutベースのレイアウトを組むのであれば、このプロパティをfalseにする必要があります。Builderを用いてViewを生成すれば、この値は自動的にfalseに設定されるのですが、コードで生成するとデフォルト値のtrueが設定されてしまします。そのため、コードだけでAutoLayoutベースのビューを生成する場合は、その都度、明示的にfalseを代入しなければなりません。loadView──viewプロパティの初期化コードだけでViewControllerを実装する場合、ViewControllerのviewプロパティもコードで設定する必要があります。viewプロパティはloadView()内で設定する必要があります。以下のコードは、UITableViewのインスタンスを生成し、viewプロパティに設定しています。https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/#//apple_ref/occ/instm/UIViewController/loadView)。UIViewControllerの上にUITableViewを配置しているだけのStoryboardがあるような場合は、このように記述してファイル数を減らしたほうがプロジェクト全体の見通しが良くなるかもしれません。viewDidLoad──viewプロパティの設定viewDidLoad()ではviewプロパティが初期化されていることが保証されているので、viewに対する設定、例えばaddSubview(_:)などはここで行います。制約の設定は、ViewControllerであればupdateViewConstraints()で、通常のViewであればupdateConstraints()の中で行います。制約が重複して追加されないように注意しましょう。(省略)}requiresConstraintBasedLayout──AutoLayoutベースのViewであることを明示するあるViewがAutoLayoutベースのレイアウトなのかどうかは、そのViewに対して、その制約を変更するような処理が実行された時点で決まります。制約が追加された時などです。これは、すべての制約をupdateConstraints()の内部で設定している場合、いつまでたってもupdateConstraints()が呼ばれず制約が設定されない、ということを意味します。このような自体を避けるために、AutoLayoutベースのUIViewのサブクラスでは、このクラスメソッドをオーバーライドしてtrueを返すようにしましょう。true}上記の問題を解消するために、setNeedsUpdateConstraints()を呼んでいるコードも時々見かけますが、厳密にはrequiresConstraintBasedLayout()を使うことが正しい方法です。requiresConstraintBasedLayout()について触れている記事はあまり見たことがないので、無視されがちなメソッドなのかもしれませんが、その挙動を正しく理解しておく必要があります。+requiresConstraintBasedLayoutは、このメソッドの挙動について検証しています。viewDidLayoutSubviews/layoutSubviews──フレームに依存する処理の記述AutoLayoutベースのレイアウトであっても、最終的なフレームに依存するコードを書くケースもあり得ます。そのようなコードは、ViewControllerではviewDidLayoutSubviews()、通常のViewではlayoutSubviews()内に記述します。label.bounds.width}このケースのように、レイアウトのどのフェーズで何が決定され、また決定されていないのか、ということを把握していないと期待する結果を得られないことがあります。コードベースのレイアウトを行う場合は尚更です。Toolboxが大変参考になります。おわりに本記事では、コードベースのレイアウトの利点と、その実装方法について説明しました。私が直面しているケースでは、コードベースのレイアウトに軍配が上がったため、また、主張を明確にするためにコードベースのレイアウトを推奨するような内容となりました。実際はプロジェクトやチーム毎に適切な判断を行うべきでしょう。GUIベースのレイアウトとコードベースのレイアウトは相反するものではありません。実現したいレイアウトにあわせて、その都度、適切な方針を選択するべきです。その上で、コードベースのレイアウトを選択することになった場合に、今回の記事が少しでも皆様の役に立てば幸いです。最後になりましたが、海外展開はまだまだ初期のフェーズであり、やらなければならないことが無数に存在します。海外の文化、ユーザーのモバイル利用環境など様々な事柄を考慮しなければならないため、必然的に開発の難易度は高くなります。18:00iOSアプリケーションでコードベースのレイアウトを積極利用するTweet関連記事2019-08-16UICollectionViewでページングスクロールを実装するこんにちは。新規サービス開発部の中村です。vol.2...こんにちは。Typeモバイル基盤部のヴァンサン(@vincentisambart)です。悩んだらこんにちは、サービス開発部の氏です。使ったセキュリティログ検索基盤の構築サービス特性にあった検索システムの設計戦略クックパッド採用説明会「クックパッドはサービスの作り手を採用したいんです。/*}*/スターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。