レイアウト実装パターン

趣味でiOSアプリ開発をかじっていた自分が、改めてiOS開発を勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

## 参考文献

この記事は以下の書籍の情報を参考にして執筆しました。

- [よくわかるAuto Layout iOSレスポンシブデザインをマスター](https://www.amazon.co.jp/%E3%82%88%E3%81%8F%E3%82%8F%E3%81%8B%E3%82%8BAuto-Layout-iOS%E3%83%AC%E3%82%B9%E3%83%9D%E3%83%B3%E3%82%B7%E3%83%96%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%82%92%E3%83%9E%E3%82%B9%E3%82%BF%E3%83%BC-%E5%B7%9D%E9%82%89-%E9%9B%84%E4%BB%8B/dp/4865940324/)

###いくつかレイアウトの実装方法について書いてます


##グループ化
UIの部品をUIViewによりグループ化し、中身のコンテンツの大きさにより高さと幅が下辺となるviewを作る。
IB上ではUILayoutGuideが使用できないのでUIViewで空間を作る。
ただし、空間の色を透明にすると描画処理が重くなってしまうので、出来れば色を指定した方がいい。

下記の例のviewは、x座標と上からの距離のみ指定されているが制約エラーは出ていない。
これは中身のviewにより高さと幅が決まっているためである。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/d9a6a5cc-a45b-23f7-185c-1bfa2b032e6c.png)


まず高さのみ制約を見ると、コンテンツの高さ+制約で高さが求められている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/4df8b7bd-e96d-67dc-02b7-14158d014f12.png)
横幅も同じようになっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/7dae6347-2d12-09e5-2659-c57936b0a9cb.png)

その他の制約
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/5d7a1729-ed92-c7cb-397f-bc663fa4c08e.png)

中身の部品はグループ化に使っているviewまたは互いに対して制約を指定しているので、画面を横にしても位置関係が変わることはない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/68b46853-d4dc-cb09-d4ac-2e72f7123939.png)


コンテンツが大きくなるとviewも大きくなる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/ff68982a-1ad9-7908-4e2b-d40ae008b5ed.png)

しかし今のレイアウトではコンテンツが入らない時に画像が隠れてしまう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/54a85631-8e0b-ec80-57f6-74f63efbe8c2.png)

はみ出した時に画像だけは画面に入るように制約を加えてみる。
親view.width >= view.widthとなるように制約を加える。
関係を >= とすることでコンテンツが小さい時は親viewのwidthに依存しないようにした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/87150e89-c6e7-4417-46b6-a89913a0df2f.png)

ラベルのTrailing Spaceの制約の優先度を下げて表示を行う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/a8a482ad-e9d1-3ecc-843e-1e1358f28820.png)

エラーがなくなって表示できるようになった。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/fbe66c32-74d4-c726-f222-db455b82710a.png)

最終的なviewと部品達の制約。特に重要なのが多い部分。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/ba56c3a2-3846-eaa9-c534-ea49bc978cea.png)

##同一幅のオブジェクト
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/3b862916-f547-0b93-7050-6454fcd56126.png)

幅を等しくするオブジェクトを全て選択して、EqualWidthを選択。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/d747ee2a-764e-beb6-8664-b6c46241cf14.png)

各々制約を設定して終わり。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/91d6161a-d796-c043-c45a-9d781c23f3e4.png)


####オブジェクト間の幅を等しくしたい場合
まずボタン同士の幅を等しくする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/f667949a-f206-18dc-56a8-9f977461d5a1.png)

viewを２つ定義してAとBの間BとCの間に設置し、それぞれボタンとくっつける制約を定義する。
（ついでに高さをボタンと揃える）
最後にview同士の幅を等しくして終わり。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/ff8626e1-1205-6050-9e49-39efe6aa9e03.png)

##トルツメパターン
トルツメ : 不用なインターフェースを削除してその間のスペースを詰めること。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/9ae687c1-71af-5969-b159-4f0c09f032df.png)
UITextViewを取り除きたい。

###トルツメ失敗パターン

####viewから消した場合

```swift
    textView.removeFromSuperview()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/8962a36f-e8d6-f84a-9b35-ecfd2a445b3b.png)

####非表示にした場合

```swift
    textView.isHidden = true
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/89bac866-9baf-d537-3a4a-8d933b44059c.png)

###viewから取り除ける場合のトルツメ
imageViewと優先度の低い制約をつける
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/fd9a8290-f804-f5db-7783-94fcde04f821.png)

viewから消す場合はトルツメできた。
非表示にする場合はできない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/69a63838-73f2-7e73-2da3-612f8fb220cb.png)

###viewから取り除けない場合のトルツメ
再利用を前提としていてオブジェクトを取り除きたくない場合、Content Sizeに高さが依存しているオブジェクトであれば、コンテンツをnilにすることでトルツメできるようになる。

####labelを取り除く場合

```swift
    label.text = nil
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/e3ad2119-7ea7-7fd0-f215-09f893ef1bf4.png)

####imageの場合

```swift
    imaageView.image = nil
```
####StackViewを使った時のトルツメ
isHiddenにするとトルツメできる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/571da46f-a604-618f-9206-3a79e107c695.png)

```swift
    imageView.isHidden = true
```
実行時
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/5e03e186-1875-b2d6-1709-f66955dbcb50.png)


##UIStackViewを使ったレイアウト
下記画像のような配置のStackViewを作りたいとする。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/ae7eeadd-d56b-99b0-499a-67a7b8270666.png)

####ダメな例
まとめて選択してStackViewにしようとする。
StackViewは部品を一方向に揃えるのでレイアウトか崩れた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/df7c9237-baad-0c12-3218-2ab1152dba3f.png)


####小さな組み合わせからまとめる
縦方向横方向に並べる部品を細分化して小さなものからStackにしていく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/956b9369-1bb0-e218-462b-17e52cb7d0c6.png)

このような見た目になるのでレイアウトを修正する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/6f59787d-7592-2d90-6718-c8826728c99c.png)

修正後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/488bea95-5f34-f730-a15f-cf067b2eef28.png)

####それぞれのStackViewの設定
####一番大きなStack
・AlignmentをFillではなくcenterにすることで各要素の幅を別々に設定できるようにした。
・DiscributuonをFillにすることでどんな画面でも最大の幅で表示するようにした。
・Spacingで各要素の間隔を取っている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/dc59e93d-fae8-54d7-5cfc-617a23f19fc0.png)

####画像をstackをまとめているstack
・AlignmentをTopにすることで文章が長くなっても画像が一番上に来るようにした。
・画像とstackの幅の比率を1:2に設定した。
・DiscributuonをFill Prportionallyにすることで画像の比率を崩さないようにした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/3811752b-a77e-1ebe-57b5-dabc6a18ebcd.png)

####星の画像をまとめているstack
・DiscributuonをEqual Spacingにしたが、他のどの設定でも良かった。

・Spacingで間隔を空けている。
・AlignmentをFillとして全ての星の画像の比率を1:1にしているが、今回はwidthを等しくする制約をつけているので、高さの制約が必要となった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/4b8324d7-c1cd-e9f7-5f0b-305827520b03.png)

widthを等しくする制約を消してstackViewの設定だけで実装しようとした時のレイアウト。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/864a375a-f3d6-f607-d087-25feee10f0ec.png)

####犬ラベルと説明textViewをまとめているstack
・Alignment、Discributuon共にFillにして横幅いっぱいに表示するようにした。
・textViewのscrolling Enabledをチェックマークを外して全文表示するようにしている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/ab8d1d8e-2e77-b3c3-8870-ae5953e9f534.png)


##コンテンツの高さに合わせてスクロールするView
画面を横にした時にコンテンツの高さだけスクロールできるViewを作る。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/4168ed8d-08d9-28e0-4a71-5fc8c558c80f.png)

Scroll View の中に View を設置し中に部品を置く。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/69d800e4-32ec-4b5b-bc7b-67cdf47bc260.png)

部品だけでViewの高さを決定するように配置を行う。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/9248bdb0-f8e4-7ec8-34c9-31c86f1046f4.png)

####Viewの制約を追加する。
FrameLayoutGuideに対して、幅を等しくする制約を与える。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/152c88b0-d905-5bae-df14-39fc259d0212.png)


ContentLayoutGuideの上下左右と等しくする制約を設定して完成。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/6fd959a2-6495-a212-1c18-7733e74d4350.png)


現在の仕様ではキーボードを表示した時に一番下のtextFieldが隠れてしまう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/2b8290e7-2d5b-6846-ac6a-5c8479cd37bc.png)

キーボードを表示した時に上にスクロールしてずらす処理を実装する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/31487160-b547-8eb7-31a0-7d8162c193b7.png)

```swift
import UIKit

class ViewController2: UIViewController, UITextFieldDelegate {
  
  @IBOutlet weak var bottomConstraint: NSLayoutConstraint!
  @IBOutlet weak var scrollView: UIScrollView!
  
  @IBOutlet weak var textField1: UITextField!
  @IBOutlet weak var textField2: UITextField!
  @IBOutlet weak var textField3: UITextField!
  @IBOutlet weak var textField4: UITextField!
  
  override func viewDidLoad() {
    super.viewDidLoad()
    
    textField1.delegate = self
    textField2.delegate = self
    textField3.delegate = self
    textField4.delegate = self
    
    NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
    
  }
  
  @objc func handleKeyboardWillShow(_ notification: Notification) {
    guard let userInfo = notification.userInfo as? [String: Any] else { return }
    guard let keyboardInfo = userInfo[UIResponder.keyboardFrameBeginUserInfoKey] as? NSValue else { return }
    guard let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double else { return }
    scrollIndicatorInsets(height: keyboardInfo.cgRectValue.size.height, duration: duration)
  }
  
  func scrollIndicatorInsets(height: CGFloat, duration: Double) {
    let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: height, right: 0)
    
    UIView.animate(withDuration: duration){
      self.scrollView.contentInset = contentInsets
      self.scrollView.scrollIndicatorInsets = contentInsets
      self.view.layoutIfNeeded()  
    }
  }
  
  @objc func handleKeyboardWillHide(_ notification: Notification) {
    scrollView.contentInset = .zero
    scrollView.scrollIndicatorInsets = .zero
  }
  
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    // キーボードを閉じる
    textField.resignFirstResponder()
    return true
  }
  
}
```

##コンテンツの大きさに合わせたTableView

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/3c132cbe-a942-ee16-7813-cd35b4e28086.png)

TableViewCellクラスを作成してxibファイルと紐づける。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/2685bcae-ba7d-89bf-405b-aec16a720644.png)


```swift
import UIKit

class TableViewCell: UITableViewCell {
    
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var textView: UITextView!
  
  func setCell(label: String, text: String) {
      self.label.text = label
      self.textView.text = text
    }

}
```

tableViewを作る。先ほど作成したカスタムセルを呼び出す。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542614/57d1929b-c5fd-1348-a036-5de593e3532a.png)

####UIViewControllerをStroryBoardで定義している場合

```swift
import UIKit

class ViewController2: UIViewController, UITableViewDelegate, UITableViewDataSource{
  
  // 表示するデータ
  let displayList = [["1", "hoge\nhoge\nhoge"],["2", "fuga\nfuga"],["3", "piyo"],["4", "a\naa\naaa"]]
  
  @IBOutlet weak var tableView: UITableView!
  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.register (UINib(nibName: "TableViewCell", bundle: nil),forCellReuseIdentifier:"TableViewCell")
  }
  
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }
  
  // セルの個数を指定するデリゲートメソッド（必須）
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return displayList.count
  }
  
  // セルに値を設定するデータソースメソッド（必須）
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // セルを取得する
    let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell") as! TableViewCell
    cell.setCell(label: displayList[indexPath.row][0], text: displayList[indexPath.row][1])
    return cell
  }

}
```
####UIViewControllerをXibで定義している場合

viewDidLoadで下記の２行を追加

```swift
    tableView.delegate = self
    tableView.dataSource = self
```