渋谷でswiftなどのiOSやAppleにまつわることをお酒を飲みながら共有するshibuya.swifit #2に参加してきたのでそのメモです。
DDD pracrice in iOS
設計の悩みどころ
- Fat ViewController
- ViewControllerが沢山フラグもった状態管理
- ユニットテストなんてかけないし、書いたとしてもテストするにはUIViewController、ネットワーク通信が必要
設計
- 英会話サプリはswift1.2で作りました
- 業務ロジックをドメインモデルにしてそれをもとにして考えていくドメイン駆動設計で作りました
レイヤー化アーキテクチャ
- プレゼンテーション層、ドメイン層、インフラ層に分ける
- プレゼンテーション層は画面の描画やユーザのイベントを受ける
- ドメイン層は実行したい処理や業務ロジック、データ定義を行う
- UseCase:処理を行うクラス
- Entity: 各種、通信したデータ、Userモデルなど
- ValueObject: ユニーク性のないデータ(CGRect、1〜5までのRangeなどのStruct)
- インフラ層ではデータ通信、永続化などを行う
リスニングの会話を聞く画面
- 環境音、会話文、など音声が再生されていて画面の状態も切り替わっているページについてユニットテストをどう書くか?
- 例えば「ボタンを押した時に音が再生されること」などをテストしたい場合、ボタンにどうアクセスするか?
- DDDでは関心事の分離で考える
- 簡素化すると、キャラクターの画像、各種ボタンがあるという画面になる
- 更に分離すると、キャラクターモデル(entityとしてあるか)、音楽を再生する機能、音楽を停止する機能、音楽を頭だしする機能、環境音を再生する機能などに分離でき、これがUseCaseになる
- 分離すると、ViewControllerではUseCaseのメソッドを呼ぶだけになる
- UseCaseにはviewにまつわるものは一切出てこない
- UnitTestで再生中になるか、停止中になるかとテストに書くことができるようになる
ViewControllerとUseCaseの接続
- 例えばボタンを停止した時にボタンのアイコンが一時停止マークなら再生アイコンに切り替わる実装を考えた時…
- 画面側に状態を持ってしまうのは、実際に音声を再生しているかフラグをもっていないので状態が食い違うかもしれない
- UseCaseにViewControllerへの参照をもたせてしまうのは、1:1で密結合になって使いまわせないし、テストの際にViewControllerが必要になる
- 解決策としてはdelegateを使う
- 再生したらdelegateを通して再生完了を通知するようにすることでViewControllerでは再生が完了したら一時停止のアイコンにすると一行書いておけばいい
- UIViewControllerDelegateに似ている!
swiftでのdelegateパターンのデメリット
- Optionalなdelegateメソッドが用意できない
- @objc修飾子をつけないといけない
- pureswiftじゃなくなり、generics, struct, enumが使えない
- しかし、swift2.0ではprotcol extensionを用いることでoptionalのような挙動をさせることができます
SwiftにおけるClassとStructの使い分け
- classとstructの違いと使い分けについてのお話
- class = reference type
- strcut = value type
=の挙動
- struct: インスタンスが複製される
- class: インスタンスへの参照が代入され、インスタンスは複製されない
Mutability(letでの宣言)
- class:参照が定数として扱われるので、参照自体の変更は不可能だが、インスタンスの値は変更可能
- struct:インスタンス自体が定数として扱われるのでプロパティの値も変更不可(プロパティの値がvarでも不可)
Structの特徴
- =の挙動より、インスタンスの独立性が担保されやすい事がわかる
- let定数化によりインスタンスをimmutableにできるのでstrutのほうがmutabilityを管理しやすい
classとstructの使い分け
- 方針としては自分で定義する型はmutabilityをシンプルに管理したいと思うので、要件をstructで満たせないかを検討する
- classに向くケース
- cocoaのクラスを使う場合はクラスを使うしかない
- インスタンスをMutableにして共有したい場合
悩ましいケース
- structがclassのプロパティをもつ場合、letで定数化しても、class自体は参照なのでclassのプロパティは変更できてしまうのでclassのimmutableが崩れてしまう
Applicative Functors in Swift
- curry(+)<^>a<*>b について理解できるようになるまでのお話
箱
- optional、array、は箱とよく聞く
- 2(a)という値と計算が失敗したかもしれない(nilかもしれない)という文脈を持っている
- 箱に対して関数を適用するには箱から開けないといけない
- 箱には、箱は開けずに値を操作する関数が用意されている
- 例えば箱は箱はmapというメソッドを備えているので箱を開けることなく値を操作できる
Fanctor
- 箱=fanctorで、fanctorは文脈を持った値で、mapを使うと文脈を保ったまま関数にて値を適用できる
Applicative Functor
- Fanctorのすごいやつで言語仕様にはないが、Runesというライブラリを使うと実現できる
<*>
- Functorがmapを持つのと同じようにApplicative Functorは<*>を持っている。
- <*>は「箱に包まれた関数」と「箱に包まれた値」を引数にとって操作する
- 箱に入った関数を箱に入った値に、箱を開けずに適用するもの
- flatmaptとmapでの実装が<*>でシンプルに置き換えられる
Curry
- 引数が複数ある関数に対してoptionalを適用したい時は、カリー化を用いる
- 例えばsumTreeElementsという関数は3つの引数をとるが、<*>で使えるようにするには1つの引数をとるものに変換しないといけない。
- 返り値は関数であってほしいときにカリー化をすることで解決
- 複数の引数をとる関数を1つの引数をとって、残りの引数を取る関数に定義をかきかえることができる
pure
- pureはruneのメソッドで.Someと同じことをやっている
- pure文脈をもった箱であれば何でも使えるものになるので、optionalでもarrayでも式を変えずに使える
<^>
- a.map(function())のショートカットで、以下は全部同じ処理。(xとyはoptionalな値)
pure(curry(+)) x y
x.map(cuury(+)) y
curry(+) x y
つまりcurry(+)<^>a<*>bとは
- +という関数をカリー化し、Optionalのデフォルト値(.Some)でその関数を包み、<*>を使い、Optionalに包まれたaとbにunwrapすることなく+を適用するという意味でした
- アプリ化ティブスタイルを使うことで、Optionalでない値を取る関数もunwrapせずにoptionalに適用でき、複数のOptionalをunwrapすることなく計算できる
メッセージ機能改善活動
- minnneでのメッセージ機能改善について作家さんとコミュニケーションできる機能を実装した時の改善について
経緯
- バージョンアップでFlatUIにしたが、インタラクションの改善が放置気味で、要望やバグの増加があったので改善した
- メッセージ入力は改行するとtext領域を拡張するGrowingTextViewと、送信した状態と同じものを表示するリアルタイムプレビューを作った
GrowingTextView
- viewの行数が変わればtableviewも変化する
- 行数を指定すれば、top, bottomなど高さをいい感じに計算するようにした
- 改行時はカーソルを常に下にするようにしている
- 各OSのバージョンでも振る舞いが違った
- 最大行数以下の場合はviewの制約を更新し、最上部へスクロールさせる
- 最大行数を超えたらviwの制約を更新し、最下部へスクロールさせる
リアルタイムプレビュー
- 画像を投稿してもURLが入力されるだけだったので、画像が表示されるようにした。
- 画像表示の際には高さを算出してTextView領域を拡張している
- 画像選択時に画像をサーバにPOSTしていたのをpost時にまとめてpostするようにした
登録処理
- 今までは画像選択に通信して画像を登録していたのを、画像を送信して、取得した画像URLを置換していくようにした
- ここはGCDのdispatch_groupで分散的に画像をPOSTしておわったら次に処理が進むようにした
再送処理
- タイムアウト時、失敗したことを通知して再送できるようにした
一覧
- ページングは通常下に行くほど古いが、今回は上に行くと過去の情報がみれる
- 通常はfooterviewが表示されたタイミングで行う
- ヘッダに移動すればいいわけではない
- データが常時追加されていくので、reloadData()時にcontentOffsetの値がずれ、慣性がへんなかんじになった
- 標準メッセージアプリの動きを見て調査した
- 触っているときは更新していなくて、スクロールが止まったら更新することがわかった
参考
XIBをダイナミックにロードしてみた
- 動作確認を早くしたかったため、実行中のiosアプリに対して再実行することなくxibを更新することを試してみた
- アニメーション速度や移動位置をコンパイルしないで確認したかった
- XIBはデータとしてローディングできることがわかった。多分uistoryboardはできない
- iosアプリにwebdavサーバを立ち上げてコンパイルしたxibファイルをアップロードすることで更新できるようにした
- 端末のviewをアップロードしたらサーバ側で動的にviewを変えられるかもしれないが、コードをアップロードしてはいけないと規約にあるので審査にはひっかかるかも
swift3.0にむけてenumerateを使い始めよう
cf: Swiftのmap, filter, reduce(などなど)はこんな時に使う!
- swift-evolutionというリポジトリがあり、swift3.0がどういう機能が追加されるのかを見ることが出来るのでそのうち気になったところが2点
- 0004はインクリメンタルがなくなる
- 0007はC言語likeなfor文がなくなる
- for-in, forEachなど配列操作する関数があるので困らない
- 配列内の要素のindexを取得したいときはindexOfを使うが、この時にenumerateを使える
- indexと要素のtuppleを取得できる
- さらにfilterをつかうことでmapで要素のみを取り出すといった形で短くできる
- swift-evolution/proposalに目を通してなくなるかもしれない文法は使わないようにしましょう
Swift API Design Guidelines (Dec 3, 2015)
- API Design guideleineを和訳してみた
基礎
- 利用時の明確さ
- 明確さは簡潔さより重要
- ドキュメントを書く
- 全てのプロパティとメソッドに対してMarkDownなどを用いてコメントを書くように。
命名
明快な語法を推進
- 曖昧さを避ける
- 例えばemployees.remove(x)などとすると、位置の指定なのか、要素の指定なのかがわからない
- 不要な単語は省略しましょう
- 例えばremoveElement(member: Element) -> remove(member: Element)
- パラメータには役割を明確にするための補足情報を追加する
- 例えばadd(observer: NSObject, for keyPath: String) -> addObserver(_ observer: NSObject, forKeyPath path: String)
文法に正しく
- mutaitingメソッドには動詞句を使用する
- non-mutaitingなメソッドには名詞句を使用する
- mutatingメソッドが動詞句の場合、対応するnon-mutatingメソッドには「ed/ing」をつける
- protocolに関しては特徴を名詞で記述してsuffixにable, ible, ingをつける
- その他の変数、プロパティ変数は名刺で記述する
専門用語を正しく使う
- あまり知られていない専門用語を使わない
- 専門用語を使用する場合は確立された意味に従う
- 略語は避ける
- 連続したデータではListよりもArrayと名前をつけるといった、先例を受け入れる
規約
一般的な規約
- O(1)ではない計算型プロパティの複雑さは文章化する
- フリー関数よりメソッドやプロパティを選択
- ケースの規約に従う
- 同じ基本的な意味を共有するとき、メソッドはベース名を共有できる
- いろんなクラスで同じ意味で同じメソッド名で使ってもいい感じ
- フリー関数は特別な場合のみ使用される
- 明白なselfがないとき
- 関数が制約なく汎用的なとき
- 関数の構文が確立されたドメイン表記の一部の時
- それ以外はclassやstructureのメソッドにしましょう
パラメータ
引数ラベル
- 引数ラベルの存在は言語のデフォルトに従う
- メソッドや関数の最初の引数は必須の引数ラベルを持つべきではない
- メソッドや関数の他のパラメータは必須の引数ラベルを持つべきである
- イニシャライザの全てのパラメータは必須の引数ラベルを持つべきである
- 例外1
- 拡張型変換の時は省略していい
- 縮小型変換の時はinitialaizerでも引数にラベルを付ける
- 例外2
- 全てのパラメータが有効に区別できないピアノ時は引数ラベルはいらない
- 例外3
- 最初の引数がデフォルト設定された時は何がcloseするかわからなくなるので、引数ラベルをつけたりしましょう
特記事項
- 制約のない多態性に最新の注意を払う
- xcodeで表示されるようになったりするので、ドキュメントコメントツールフレンドリーにする
- 曖昧さを排除するために、appendというメソッドはやめてappendContentsOfなど、オーバーロードをより明確にする
Conclusion
- あるていどガイドラインを意識しながらコードを書いたほうが良い
- 細かい部分はチームでガイドラインを決めましょう
プログラミング初心者の壁の超え方
最低限の知識をどう獲得するか
- ドットインストールや入門本など、いろんな初心者向けの教科書を平行して使う
- とりあえず写景して動かす
- 本の学習が終わらないからアプリが作り始められないわけではない。
- 初心者には、Xcodeの使い方、クラス・インスタンス・メソッド・変数、StoryboardとUIKit、AutoLayout、UITableViewなどの情報を限定して与えるのがいい
- やりたいことは普段からためておき、できることを頑張ってギャップを埋める
動かない
- 検索を頑張り、出来ないなら詳しい人に聞いたり、StackOverFlowやDevForumで質問する
- codementorというサービスを使ってみた
- マッチング方式、相対方式と2つあるが、相対方式がおすすめ
- ScreenShotをシェアしたりMentorにコントロールも渡せる
- 最初の5分は無料
- Ashishさんは高いけどおすすめ
- 1時間詰まって解決しなかったらこれを使った
ビールを飲みながらちゃんと発表も同時に聞けるので普段の勉強会よりも楽しめた感じがあります!
普段、懇親会などは発表が終わってからやるか、もしくは発表中にのみくいを立ちながらLTをしたりなどで発表をきちんときけなかったりするのですが、飲みながらちゃんと発表が聞けるというのは2つ同時に楽しんでいる感じがあってとてもいい勉強会でした!
是非とも3回目も参加したい所存です。
View Comments
Thank you for your sharing. I am worried that I lack creative ideas. It is your article that makes me full of hope. Thank you. But, I have a question, can you help me? https://www.binance.com/sl/register?ref=V3MG69RO
Your point of view caught my eye and was very interesting. Thanks. I have a question for you.