これは Swift Tweets の発表をまとめたものです。イベントのスポンサーとして Qiita に許可をいただいた上で、このような形(ツイートの引用)で投稿しています。
Swift Tweetsオーディエンスの皆様こんばんは!Tweetupという新しい試みに参加させていただきとてもワクワクしています。本日は「Swift時代に悩ましいUIViewControllerをどう扱うか」についてご紹介させていただきます。よろしくお願いします。 #swtws pic.twitter.com/JWfOjH0E1W
— susieyy (@susieyy) 2017年1月14日
まずは自己紹介から。杉上洋平と申します。iOSの開発は日本でiPhoneが販売されたときに、嫁が早速手に入れてアプリがないので作ってほしいと言われたのをきっかけに、2008年からアプリを作り続けています。 #swtws pic.twitter.com/rDgsq1OtU8
— susieyy (@susieyy) 2017年1月14日
AppStoreでのアプリの自主販売での生計を皮切りにフリーランスを経て現在はWantedlyでアプリの開発を行っています。WantedlyではSwiftが1.0Betaのころから3本の新規アプリを開発しSwift言語の成長とエコシステムの発展と共に歩んできました。 #swtws pic.twitter.com/pYJwaP0Vqm
— susieyy (@susieyy) 2017年1月14日
アプリの開発にあたりOSSやコミュニティの皆さんに助けられて進めることができました。そのため開発で得た知見は Pay it Forwardの精神のもとQiita( https://t.co/mv6q3S2Fst )に記事を記載させていただいております。 #swtws pic.twitter.com/zGJcd758B4
— susieyy (@susieyy) 2017年1月14日
本発表では去年11月にリリースしたWantedlyPeopleというアプリの開発において、Swiftの特性を活用しつつUIViewController(以下VC)をどのように向き合うか試行錯誤した内容をご紹介します。 #swtws
— susieyy (@susieyy) 2017年1月14日
この扱い方が一般的により優れているということではなく、アプリの要件、開発規模、チームメンバー構成、設計方針により適応できるかどうかはケース・バイ・ケースになると思うので、開発の選択肢の1つとして捉えていただけると幸いです。コードはSwift2.3になります。 #swtws
— susieyy (@susieyy) 2017年1月14日
アプリ開発においてVCは画面のライフルサイクルと遷移を担い、AppleMVCパターンでアプリを構築する上で中心的な存在となっています。その上でVCは開発者にとって重量であると同時に悩ましい問題をたくさん抱える課題の宝庫でもあります。 #swtws pic.twitter.com/YpgLc5bOV3
— susieyy (@susieyy) 2017年1月14日
代表的な以下の課題を含め多様な視点でVCと試行錯誤しました。 #swtws
— susieyy (@susieyy) 2017年1月14日
- SwiftとStoryBoardの相性が悪い
- VCの肥大化問題と抽象化・共通化をどのように行うか
- 画面間(VC)を疎結合に扱いたい
- Depplinkで任意画面へのダイレクトな遷移を行いたい
iOSの開発ではStoryBoardを活用して画面の構成を構築する方が多いのではないでしょうか。StoryBoardは直感的な操作でエンジニア、デザイナー共に扱いやすくビューの配置やレイアウトを視覚的に配置・把握が行える素晴らしいツールです。 #swtws pic.twitter.com/IVe5uasrdi
— susieyy (@susieyy) 2017年1月14日
AutoLayoutの設定ではリアルタイムで整合性を検証してくれるためあるべき正しい設定へと導いてくれます。また画面間の遷移も設定が行え画面遷移図としての仕様としても重宝します。そんな素敵なStoryBoardですが万能ではありません。使いづらい点を見てみましょう。 #swtws
— susieyy (@susieyy) 2017年1月14日
StoryBoardとSegueによる画面遷移を行うコードです。画面遷移決定時に遷移先の画面へ渡したい引数が確定することが都度だと思いますが引数の設定はprepareメソッド内で行うため、渡したい引数はsenderというAny?型で引き回す必要があります。 #swtws pic.twitter.com/MrLqKzz2hP
— susieyy (@susieyy) 2017年1月14日
このときいくつかの悩ましい問題に直面します。 #swtws
— susieyy (@susieyy) 2017年1月14日
- sender引数を利用すると遷移先の画面へ渡したい引数の型が失われてしまう、引数が複数の場合Dictionary型として扱う
- prepareメソッドで責務が集約されているように見えるけどなんとなく冗長
- Identifierは文字列型を取るためコンパイラチェックできない
— susieyy (@susieyy) 2017年1月14日
- SecondViewControllerのinitをコードから呼び出すことはできない、そのため初期化後のインスタンスのプロパティへ代入することで代替する必要がある
#swtws
また画面遷移先であるSecondViewControllerのプロパティtitleNameは、初期値が不変でイミュータブルな場合でもシンプルにlet定数で定義できずForceUnwrapか再代入が不要でもvar変数として定義する必要があります。 #swtws pic.twitter.com/LbLIK6rSIQ
— susieyy (@susieyy) 2017年1月14日
イミュータブルであることは保証したいのでForceUnwrapを記述してお茶を濁すケースが多いのではないでしょうか。ただしForceUnwrapで定義したlet定数はviewDidloadなどで呼び出される前に事前に初期化しておく必要があります。 #swtws
— susieyy (@susieyy) 2017年1月14日
そのため以下のようなコードではクラッシュをしてしまいます。この点はコンパイル時に抑止できないので、プログラマーがしっかり安全性を担保する必要があります。 #swtws pic.twitter.com/SV0yWhYRhb
— susieyy (@susieyy) 2017年1月14日
なんとStoryBoardはSwiftと相性が悪いことでしょう。この嘆かわしい状況に1点の光を指し示す発表が昨年のTry!Swiftでありました。「実践的 Boundaries」の中で紹介されたImmutable Coreの考え方です。 #swtws pic.twitter.com/gZd7hxABVA
— susieyy (@susieyy) 2017年1月14日
StoryBoardを利用せずVCのinitを活用したコードは以下のようになります。required initの記述など冗長な点はありますが我慢できる範囲内です。 #swtws pic.twitter.com/L4rLBt92gR
— susieyy (@susieyy) 2017年1月14日
僕は思いました。そうだStoryBoardを捨てよう。。。よりSwiftらしくVCと向き合うために。型安全でイミュータブルで、プログラマーによる担保ではなく、コンパイラによる不整合の発生しないことが保証される世界のために。 #swtws
— susieyy (@susieyy) 2017年1月14日
ただし、この選択は上述のメリットと、裏腹にStoryBoardを利用できないというトレードオフによるデメリットの両方を内包します。以降ではデメリット部分とその対策について見ていきます。 #swtws
— susieyy (@susieyy) 2017年1月14日
StoryBoardを活用しないとアプリの画面遷移がわかりにくくなるという課題があります。StoryBoardがないために途中参加のメンバーがある画面がどのVCに紐づくのかわからないため、ログを出してひとつづつ確認していったという話も聞いたことがあります。 #swtws
— susieyy (@susieyy) 2017年1月14日
StoryBoardはそれ自体が実装なので画面遷移図として見たときも図(仕様書)と挙動が乖離しないメリットがありますが、ここではStoryBoardを画面遷移図とする代替手段として画面遷移図を把握するための2つのツールをご紹介します。 #swtws
— susieyy (@susieyy) 2017年1月14日
1つ目はGoodpatchのProttというプロトタイプツールです。iPhoneから利用し画面の一部をタップすると指定した画面へ遷移する動きを実際のアプリさながらに確認できます。この遷移の設定を行うと自動的に各画面が重ならないような画面遷移図を自動生成してくれます。 #swtws pic.twitter.com/bkQZUf5lGi
— susieyy (@susieyy) 2017年1月14日
初期開発から導入しておけば、デザイン ➜ プロトタイプ/画面遷移図 ➜ iOS実装の流れで開発が進められるので大変重宝します。また遷移の変更などメンテナンスもしやすくワイヤーの画面遷移図よりも視認性も高くいので素晴らしい仕様書となります。 #swtws
— susieyy (@susieyy) 2017年1月14日
2つ目は37signalsで紹介されているUI Flowです。UI Flowの記述はテキストで行えるguiflowを利用しています。新規登録ログインなど条件により複雑に遷移先が変わる遷移部分について活用しています。 #swtwshttps://t.co/ByfACqRuV1 pic.twitter.com/abAQqPDuEP
— susieyy (@susieyy) 2017年1月14日
画面の遷移はStoryBoardのSegueや画面遷移図の遷移順だけではなく、アプリ外でのユニバーサルリンクやプッシュタップによって任意画面ダイレクトに遷移したい場合もあるでしょう。さらにその画面への遷移に伴う一連の遷移スタックも保持したい場合もあるかもしれません。 #swtws pic.twitter.com/rIBE7r8RGd
— susieyy (@susieyy) 2017年1月14日
このような場合にSegueベースの遷移ではスタックを保持しつつの遷移は扱いにくいのですが、VCのinitで初期化するコードではシンプルなので比較的扱いやすいです。 #swtws pic.twitter.com/tjekd7WIWv
— susieyy (@susieyy) 2017年1月14日
先程のコードは一見うまく動作しそうに見えますが実はバグを孕んでいます。rootViewControllerのナビゲーションが起点のVCの表示ではなく、すでにある画面へ遷移していたりあるモーダルの表示がある場合は遷移スタックに不整合が発生します。 #swtws
— susieyy (@susieyy) 2017年1月14日
しかもモーダルの表示はVCに限らずUIAlertControllerによるアラートやアクションシートも含まれます。そのため再帰的にすべてのモーダルをdismissしたり、すべてのナビゲーションをポップしたりする必要があるのですが漏れの危険もあり骨が折れます。 #swtws
— susieyy (@susieyy) 2017年1月14日
そのため起点のVCを作り直してしまうことで現状の遷移状態を破棄し1から画面と遷移を構築します。このような処理が頻繁に発生する場合はVCのメモリリークがないかチェックもあわせて行います。 #swtws pic.twitter.com/vOipXUBbV6
— susieyy (@susieyy) 2017年1月14日
上記のような起点からVCを作りなおす実装を行うに限らず、すべてのVCがしっかりdeinitが呼ばれているかを確認することはとても大事なことです。普段から画面を閉じたり、戻った場合はVCのdeinitが呼ばれているかログなどでこまめに確認するのがよいでしょう。 #swtws pic.twitter.com/AieonKE1nz
— susieyy (@susieyy) 2017年1月14日
ダイレクトな遷移先が1つだとシンプルで良いのですが、多義に渡る遷移パターンがある場合は、ルーティングを担うロジックを導入することを検討すると良いでしょう。ライブラリがいくつかありますが、Swiftはenumが強力なので自前実装でも十分対応可能です。 #swtws pic.twitter.com/H2B8rTqURb
— susieyy (@susieyy) 2017年1月14日
URIベースのルーティングテーブルはパスとIDで構成されます。パスは対応するVCのクラスを一意に定めます。VCが必要なデータそのものを引数とせず、データのIDのみを引数にとる設計だとURI情報のみである画面へ遷移が可能になりよりルーティングと相性がよくなります。 #swtws pic.twitter.com/nE7dY5kMOk
— susieyy (@susieyy) 2017年1月14日
VCはデータのIDのみを自身の初期化時に受け付けて紐づく必要なデータをローカルのDBや通などから取得します。ルーティングや遷移元のVCから、遷移先のVCへ依存はIDのみになるので疎結合であり独立生が高くVCの責務がVCに閉じる設計となります。 #swtws
— susieyy (@susieyy) 2017年1月14日
また、VCの初期化時の引数により内部の分岐するような場合は、引数をenumで定義して分岐処理部分を極力enumに実装することでVCの可読性が高くなります。以下は1つのVCで自分と他人のプロフィールを表示する例です。次のtweetに続きます。 #swtws pic.twitter.com/33eQRuLEl0
— susieyy (@susieyy) 2017年1月14日
自分か他人かの差異はenumに閉じているので、VCではその差異を意識せず見通しの良い記述が行えます。 #swtws pic.twitter.com/g6Xk12iXgC
— susieyy (@susieyy) 2017年1月14日
さて、StoryBoardは使わない方針としていますが、やはり部分的にInterfaceBuilderを利用してビューのデザインを行いたいケースも多々あります。そのような場合はVCに紐づくStoryBoardで構築するのではなくビューに紐づくXIBを利用しています。 #swtws
— susieyy (@susieyy) 2017年1月14日
XIBの初期化を行うコードは以下のように記述が長くForceUnwrapもあり、引数のnibNameは文字列をとるなど頻繁に記述するには使い勝手がよくありません。 #swtws pic.twitter.com/CH8WgIrUql
— susieyy (@susieyy) 2017年1月14日
そこで、Protocolを活用して記述しやすくかつ、ForeUnwarapを局所化しています。ビューのクラス名とXIBのファイル名を揃えることで文字列の介入を防いでいます。 #swtws pic.twitter.com/MEqLtlR33u
— susieyy (@susieyy) 2017年1月14日
以下のようにInstantiatableFromNibプロトコルに準拠したビューを定義します。XIBの初期化を行うコードは文字列とForceUnwrapが排除され記述しやすく可読性も向上しました。 #swtws pic.twitter.com/ks9jXTgYGW
— susieyy (@susieyy) 2017年1月14日
InterfaceBuilderを利用してビューの生成、AutoLayoutの設定を行うのもよいですが、強力なSwiftの世界で行えないものでしょうか。以降では左記の2点(AutoLayout、ビューの生成)について掘り下げていきます。 #swtws
— susieyy (@susieyy) 2017年1月14日
Swiftと言えどもAutoLayoutをそのままコードで扱うのはとても辛いです。以下はparentViewに追加したchildViewがparentViewと同じ4辺を共有するレイアウトの例です。2行ぐらいで済みそうな記述なのにこんなに記述する必用があります。 #swtws pic.twitter.com/rki39eNnWz
— susieyy (@susieyy) 2017年1月14日
そこでSnpaKitというライブラリを活用します。先程と同等のコードが端的で直感的な記述で行うことができます。非常に簡単にAutoLayoutを記述できるので自分はXIBによるレイアウトはごく一部でほとんどのAuotLayoutをSnapKitで記述しています。 #swtws pic.twitter.com/FTytHLJEmW
— susieyy (@susieyy) 2017年1月14日
ビューのレイアウトの次は、コードで行うビューの生成について見ていきましょう。以下は生成とAutoLayoutを行うコードです。ViewDidLoad内ですべて記述しているので画面の規模が大きくなると煩雑になりそうです。 #swtws pic.twitter.com/SMMM94zx2f
— susieyy (@susieyy) 2017年1月14日
そこでビューの生成部分をクロージャーによるデフォルトプロパティを活用して生成と初期設定値をまとめて記述します。ViewDidLoadにいくつものビューを手続き的に記述するよりもビュー単位の初期化コードの範囲が明確に局所化され可読性も向上します。 #swtws pic.twitter.com/d8oPZENdXn
— susieyy (@susieyy) 2017年1月14日
注意点としてクロージャが実行される時点ではVCインスタンスが保有するその他のプロパティは初期化されていません。つまりクロージャ内部ではその他のプロパティへアクセスすることが出来ません。ということはselfプロパティも使えませんし、インスタンスメソッドも使えません。 #swtws
— susieyy (@susieyy) 2017年1月14日
画面間の遷移がサクサクと滑らかに感じられるとユーザの体験は向上します。もしある画面への遷移時にもたつきを感じ、ある画面が遷移の初期状態で表示しないビューがいくつかある場合は、遷移時に表示しないビューを初期化しないことで遷移の速度を向上できることがあります。 #swtws
— susieyy (@susieyy) 2017年1月14日
上記は遅延評価プロパティを活用することで容易に記述できます。 #swtws
— susieyy (@susieyy) 2017年1月14日
- 初期値の設定処理を参照が発生するまで遅延できる
- デフォルトプロパティと異なりselfを参照できる
- クロージャー内のself参照は循環参照を発生しない
遅延評価については、Kumagaiさんが大変すばらしい資料にまとめてくださっているのでご参照ください。 #swtwshttps://t.co/JlJ0H11o6D
— susieyy (@susieyy) 2017年1月14日
以下のコードはemptyViewの初期化を通信の結果件数が0件であると評価するまで遅延する例です。 #swtws pic.twitter.com/dlfKaYaWnf
— susieyy (@susieyy) 2017年1月14日
ビューの遅延評価による初期化だけでなく、子のVCの初期化遅延にも活用できます。 #swtws pic.twitter.com/N5z4mJXIId
— susieyy (@susieyy) 2017年1月14日
UITableViewはアプリにおいて頻出のコンポーネントです。画面のVCをUITbaleViewController(以下TVC)を継承して開発する方も多いのではないでしょうか。 そうするといくつか問題が発生することがあるのでTVCを継承しなくなりました。 #swtws
— susieyy (@susieyy) 2017年1月14日
TVCを継承するとtablewViewがVCの起点ビューとなるため後々のUI/UXの変更によりtablewViewと並列やtablewViewよりも背後のビューを生成できずに作りなおすはめになることがあるからです。なのでTVCを子のVCとして扱うようにしています。 #swtws pic.twitter.com/orkWMZznSP
— susieyy (@susieyy) 2017年1月14日
ちなみにtableViewのみを作成せずにUITableViewControllerを活用しているのはセルをタップしてUINavigationController#pushで遷移してpopで戻ってきた場合にセルのハイライトをアニメショーン付きでオフにしたいためです。 #swtws
— susieyy (@susieyy) 2017年1月14日
TVCを継承しない話をしましたが、僕は基本的にアブストラクトなVCを設けて処理を共通化する方法は行わないようにしています。ある画面のVCは常にUIViewControllerを継承し、finalを明記することで自身も継承させてません。 #swtws
— susieyy (@susieyy) 2017年1月14日
VCである処理を共通化したい場合は、独自のビューを持つ場合は任意のVCクラスと切り出して子のVCとして扱うか、プロトコルによる抽象化とプロトコルエクステンションによる実装を行います。これらにより肥大化しやすいVCをコンパクトに扱うよう心がけています。 #swtws pic.twitter.com/X416MrnBvq
— susieyy (@susieyy) 2017年1月14日
継承ではなくプロトコルによる共通化を行うのは共通化したい対象を責務の範囲として局所化しやすいためです。プロトコルはVCに多数適用できますが、継承だとどうしても最大公約数的な共通処理のアブストラクトなVCを継承しがちで多様な共通処理がクラスに記述されてしまいます。 #swtws
— susieyy (@susieyy) 2017年1月14日
UIViewControllerをextensionすることで共通化を行うこともできますが、実装した影響範囲がすべてのVCになるので本当にすべてのVCで利用するような処理だけを記述するようにしています。 #swtws pic.twitter.com/uHDTry1zYe
— susieyy (@susieyy) 2017年1月14日
以上で「Swift時代に悩ましいUIViewControllerをどう扱うか」終了です。ObC時代から苦しめられたVCですがSwiftのお陰で悩みも軽減された感じがします。それでは、ごTweet聴ありがとうございました。m(_ _)m #swtws pic.twitter.com/0FtaoCUqyd
— susieyy (@susieyy) 2017年1月14日