Auto Layout を使わずにわかりやすいコードでレイアウトが組めるフレームワーク NotAutoLayout

# 前書き
[以前書いた記事](http://qiita.com/lovee/items/acfc9d0f1ffa7207b38b)の通り、筆者は Storyboard も AutoLayout も大嫌いです。自分のプロジェクトには基本的に全てコードでレイアウト組んでます。
かと言ってレイアウトのために `ViewController` ごとに `UIView` を作るのもなんかだるいし、`ViewController` の中に子 `ViewController` が含まれた場合 `View` と `View` の関係もなんか複雑になってくるのでややこしくなりかねない難点があります。
なのでレイアウトだけのために `UIViewController` ごとに `UIView` をサブクラスしなくてもいいように、`UIViewController` からレイアウトに必要な情報を渡して正しくレイアウトしてくれる `UIView` を作って Framework にまとめることにしました。

# 目標
- コントローラーの中に子ビューと一緒に子ビューの必要なレイアウト情報を渡すだけで済む
- レイアウト情報はなるべく見やすく、かつ一箇所にまとめられるようにする
- 自由度があり、親ビューの大きさに応じて正しくレイアウトが組める
- レスポンシブレイアウトもスマートに組める
- でもなるべくコード量を抑えたい

# 完成物
[GitHub](https://github.com/el-hoshino/NotAutoLayout) にあります。

# 解説（20170226書き直し）
以前の記事にも解説書きましたが、当時のバージョンと比べて現在の 0.7 版はだいぶ変わったので解説書き直します。

## 変更点
まず `LayoutView` というクラスはまだ存続していますが、それは `UIView` から継承しただけでなく、今回新たに追加した `LayoutControllable` というプロトコルに準拠しました。基本的に具体的なレイアウトのやり方は全て `LayoutControllable` の `extension` で実装しましたので、これにより `UIView` 以外のビュー、例えば `UIImageVew` などでも自分で作って `LayoutControllable` に準拠すれば OK です。

次に当時のバージョンにあった `layoutingSubviews: [(view: UIView, layoutMethods: [LayoutMethod])]` というプロパティーがなくなって、その代わり `layoutInfo: [UIViwe: [LayoutMethod]]` と `zIndexInfo: [UIView: Int]` という二つの辞書型プロパティーが使われることになりました。これにより前は `layoutingSubviews` というプロパティーに追加しなければならないという通常の子ビューの追加の仕方とはちょっと違うという問題が解消され、今は通常の子ビューの追加と同じ `addSubview` でできるようになりました。

また以前はほぼ全ての型は `LayoutView` の下にぶら下げていましたが、今は全部上に出して、`LayoutView.LayoutMethod` ではなく普通に `LayoutMethod` で書けば OK です。

## 使い方
NotAutoLayout を `import` したら、`LayoutView` という `UIView` のサブクラスが使えるようになります。

`LayoutView` には `layoutInfo` と `zIndexInfo` というプロパティーがあります、中にはそれぞれ子ビューのレイアウトに必要な情報（`[LayoutMethod]`）と zIndex の情報（`Int`）が入っています。

`LayoutMethod` は `(condition: (CGSize) -> Bool, position: LayoutPosition)` の `typealias` です。`condition` は現在の `LayoutView` のサイズから求められたレイアウト条件で、`position` は具体的なレイアウト方法です。例えば `condition` が `{$0.width > $0.height}` と書くと、`LayoutView` のサイズが高さより幅の方が大きい、つまりランドスケープの場合に `position` を求めてレイアウトしますし、`condition` が `{$0.width > 1024 && $0.height > 768}` と書くとこれは画面サイズが `1024x768` より大きい場合に `position` を求めてレイアウトします。さらには条件を特に決めたくなければ `{_ in true}` と書けば無条件に `position` を求めます。

ただし注意しなければならないのは `layoutMethods` は `LayoutMethod` の配列ですので、`first(where:)` メソッドで配列の中に一番最初に条件に合った `LayoutMethod` だけが求められます。また配列に条件にあった `LayoutMethod` がない場合は再レイアウトしません。

そして条件に合ったメソッドが見つかった場合、メソッドの `position` を基にレイアウトを決めますが、この `position` はさらに `Position` という `enum` 型となります。中には 
- `.absolute(CGRect)`
- `.relative(CGRect)`
- `.insets(UIEdgeInsets)`
- `.offset(value: UIOffset, from: OffsetOrigin, size: CGSize)`
- `.customByFrame(frame: (CGSize) -> CGRect)`
- `.customByOriginSize(origin: (CGSize) -> CGPoint, size: (CGSize) -> CGSize)`
- `.customByXYWidthHeight(x: (CGSize) -> CGFloat, y: (CGSize) -> CGFloat, width: (CGSize) -> CGFloat, height: (CGSize) -> CGFloat)`
の七つの `case` があります。

- `absolute` は `LayoutView` のサイズと関係なく `CGRect` の数値のみでレイアウトを決めます。例えば `.absolute(CGRect(x: 10, y: 20, width: 30, height: 40))` を使えば、`LayoutView` のサイズ関係なく指定の子ビューの `frame` は常に `CGRect(x: 10, y: 20, width: 30, height: 40)` になります。
- `relative` の `CGRect` の数値は全て `LayoutView` のサイズから見た相対的な大きさで、例えば `.relative(CGRect(x:0.1, y: 0.2, width: 0.3, height: 0.4))` 、`LayoutView` の `bounds.size` は `CGSize(width: 100, height: 200)` の場合、指定の子ビューの `frame` は `CGRect(x: 100 * 0.1, y: 200 * 0.2, width: 100 * 0.3, height: 200 * 0.4)` になります。
- `insets` は `LayoutView` から見た余白の指定になります。例えば `insets` は `UIEdgeInsets(top: 10, left: 20, bottom: 30, right: 40)`、`LayoutView` の `bounds.size` は `CGSize(width: 100, height: 200)` の場合、指定した子ビューの `frame` は `CGRect(x: 20, y: 10, width: 100 - 20 - 40, height: 200 - 10 - 30)` になります
- `offset` は指定した基準点からのオフセットとサイズで指定した子ビューの `frame` が決まります。基準点である `OffsetOrigin` は `.topLeft`、`.topCenter` 等の 9 パターンがあります。例えばここで `value` が `UIOffset(horizontal: -10, vertical: 10)`、`from` が `.topRight`、`size` が `CGSize(width: 30, height: 40)` で、`LayoutView` の `bounds.size` が `CGSize(width: 100, height: 200)` の場合、指定した子ビューの `frame` は `CGRect(x: 100 - 30 + (-10), y: 10, width: 30, height: 40)` になります。
- `customByXXXX` はその都度 `LayoutView` の `bounds.size` をクロージャーに渡して必要なフレームを求める非常に自由度の高い指定法です。違うのは戻り値は `CGRect` なのか、それとも `CGRect` の構成要素なのかです。なぜこんな風に作ったかというと物によっては `frame` ではなく `x, y, width, height` の方が書きやすかったりする場合があるからです。また、クロージャーを用いるので非常に自由度は高いですが、逆にこれは `layoutInfo` に格納されているので、必要に応じてキャプチャーリストを入れてあげないとメモリリークになりますのでご注意を。

また、`position` に使う `CGRect` は `frame` プロパティーと同じように、左上の原点座標とサイズを記述する書き方ですが、中身は直接 `frame` プロパティーにではなく、`bounds` と `center` にそれぞれ適用していますので、`transform` が入っても問題なく表示できます（多分…）

直接 `[LayoutMethod]` を書くのは物によってソースコードが煩雑化する可能性が高いので、便利なメソッドもいくつか用意してあります：

- `setLayoutMethods(_ methods: [LayoutMethod], for subview: UIView)` は `subview` に `methods` を関連づけ、もし `subview` にはすでに他の `[LayoutMethod]` と関連付けされた場合は古い値は破棄されます。
- `setConstantPosition(_ position: LayoutPosition, for subview: UIView)` は `subview` に `(condition: {_ in true}, position: position)` という `LayoutMethod` と関連付け、もし `subview` にはすでに他の `[LayoutMethod]` と関連付けされた場合は古い値は破棄されます。
- `appendLayoutMethod(_ method: LayoutMethod, for subview: UIView)` は `subview` に既存の関連付けされた `[LayoutMethod]` に新たに `method` を追加するメソッドです。もし既存の `[LayoutMethod]` がなければ `[method]` を関連付けします。
- `appendConstantPosition(_ position: LayoutPosition, for subview: UIView)` は `subview` に既存の関連付けされた `[LayoutMethod]` に新たに `(condition: {_ in true}, position: position)` という `LayoutMethod` を追加するメソッドです。もし既存の `[LayoutMethod]` がなければ `[(condition: {_ in true}, position: position)]` を関連付けします。
- `setLayout(of subview: UIView, at position: LayoutPosition, while condition: @escaping LayoutCondition)` は `appendLayoutMethod(_ method: LayoutMethod, for subview: UIView)` の別バージョンです。

再度レイアウトが必要な場合は通常の `UIView` と同じようにメインスレッドから `setNeedsLayout()` を呼べばもう一度レイアウト情報通りにレイアウトしてくれます。画面の向きを変えた場合は OS が勝手に `layoutSubviews()` を呼び出してくれるので手動で呼び出す必要がありません。

また、ここでは `LayoutView` のインスタンスをそのまま利用していることを前提に解説していますが、`LayoutView` をサブクラスしても、もしくは他の自分のクラスを `LayoutControllable` に準拠させるのも可能です。`LayoutControllable` に準拠させる場合は `layoutInfo` と `zIndexInfo` の格納プロパティーを作って、レイアウトメソッド内に（`UIView` なら `layoutSubviews()`）`layoutControl()` を呼び出せば OK です。`LayoutView` の実装を読んでみればわかると思います。

なお、`zIndex` についてですが、これは `UIKit` は `zIndex` や `zPosition` といった奥行き情報に対応していないため、基本手動で `reloadSubviews()` を呼び出さないと `zIndexInfo` の情報が反映されません。なのでどっちかというと子ビューを追加するときに順番通りに `addSubview` もしくは `insertSubview` などで入れることがオススメです。`reloadSubviews()` を呼び出すと、現在の `LayoutView` に追加された全ての `subvuews` を一旦 `remove` して、そして `zIndexInfo` からそれら奥行き情報を参照し（なければデフォルト値の 0 とみなす）、小さい順でソートし直し（`Int` 型なのでマイナスの値を与えるとデフォルト値より奥に配置されます）、ソートされた順番でもう一度 `addSubview` されます。

# 使用例
例えば、下記の図のように、ラベルをキャンバスの幅の 20% の正方形、また右上に 10 ピクセルの余白をつけたい場合
![スクリーンショット 2017-02-26 12.55.28.png](https://qiita-image-store.s3.amazonaws.com/0/63998/46faba02-6b7c-600f-7d93-e8ad2dea43e8.png)

Playground でこれを書いてみるとこんな感じになります：

```swift:Playground
import UIKit
import PlaygroundSupport
import NotAutoLayout

let view = LayoutView(frame: CGRect(x: 0, y: 0, width: 320, height: 568))
view.backgroundColor = .white
PlaygroundPage.current.liveView = view

let label: UILabel = {
	let label = UILabel()
	label.backgroundColor = .red
	label.textAlignment = .center
	label.text = "Label A"
	view.addSubview(label)
	return label
}()

let position = LayoutPosition.customByXYWidthHeight(x: { $0.width * 0.8 - 10 },
                                                    y: { _ in 10 },
                                                    width: { $0.width * 0.2 },
                                                    height: { $0.width * 0.2 })
view.setConstantPosition(position, for: label)

view.setNeedsLayout()
```

といった感じになります。`UIViewController` なら `loadView()` をオーバーライドして `view` を `LayoutView` にすればいいでしょう。

また、計算量が増えるにもかかわらず、`LayoutView` にレイアウト条件を入れるのではなく、子ビューにレイアウト条件を入れるのはその方が子ビューにとってもっとレイアウトの自由度が増えるし（例えば特定な子ビューを画面サイズだけでなく他の条件を応じたレイアウトも可能）、子ビューの要素がそこまで増えなければ 0.01 秒の誤差なんて無視しても問題ないからです。それに `LayoutView` をネストすることも可能ですので、ネストをうまく使えばレイアウト条件の計算を減らすことも可能かと思います。

# あとがき
`relative` と `absolute` の単語は CSS からパクってきたけど意味としては CSS のそれとは違うので微妙に気持ち悪いっちゃ悪い。何かもっとふさわしい単語のオススメがあれば教えていただきたいです。
他にももし何か質問あれば気軽にどうぞ。
**[GitHub](https://github.com/el-hoshino/NotAutoLayout) で Star くれるととても喜びます。**