LoginSignup
5
3

More than 3 years have passed since last update.

レイアウト実装パターン

Last updated at Posted at 2020-01-09

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

参考文献

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

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

グループ化

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

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

image.png

まず高さのみ制約を見ると、コンテンツの高さ+制約で高さが求められている。
image.png
横幅も同じようになっている。
image.png

その他の制約
image.png

中身の部品はグループ化に使っているviewまたは互いに対して制約を指定しているので、画面を横にしても位置関係が変わることはない。
image.png

コンテンツが大きくなるとviewも大きくなる。
image.png

しかし今のレイアウトではコンテンツが入らない時に画像が隠れてしまう。
image.png

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

image.png

ラベルのTrailing Spaceの制約の優先度を下げて表示を行う。
image.png

エラーがなくなって表示できるようになった。
image.png

最終的なviewと部品達の制約。特に重要なのが多い部分。
image.png

同一幅のオブジェクト

image.png

幅を等しくするオブジェクトを全て選択して、EqualWidthを選択。
image.png

各々制約を設定して終わり。
image.png

オブジェクト間の幅を等しくしたい場合

まずボタン同士の幅を等しくする。
image.png

viewを2つ定義してAとBの間BとCの間に設置し、それぞれボタンとくっつける制約を定義する。
(ついでに高さをボタンと揃える)
最後にview同士の幅を等しくして終わり。
image.png

トルツメパターン

トルツメ : 不用なインターフェースを削除してその間のスペースを詰めること。
image.png
UITextViewを取り除きたい。

トルツメ失敗パターン

viewから消した場合

    textView.removeFromSuperview()

image.png

非表示にした場合

    textView.isHidden = true

image.png

viewから取り除ける場合のトルツメ

imageViewと優先度の低い制約をつける
image.png

viewから消す場合はトルツメできた。
非表示にする場合はできない。
image.png

viewから取り除けない場合のトルツメ

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

labelを取り除く場合

    label.text = nil

image.png

imageの場合

    imaageView.image = nil

StackViewを使った時のトルツメ

isHiddenにするとトルツメできる。
image.png

    imageView.isHidden = true

実行時
image.png

UIStackViewを使ったレイアウト

下記画像のような配置のStackViewを作りたいとする。
image.png

ダメな例

まとめて選択してStackViewにしようとする。
StackViewは部品を一方向に揃えるのでレイアウトか崩れた。
image.png

小さな組み合わせからまとめる

縦方向横方向に並べる部品を細分化して小さなものからStackにしていく。
image.png

このような見た目になるのでレイアウトを修正する。
image.png

修正後
image.png

それぞれのStackViewの設定

一番大きなStack

・AlignmentをFillではなくcenterにすることで各要素の幅を別々に設定できるようにした。
・DiscributuonをFillにすることでどんな画面でも最大の幅で表示するようにした。
・Spacingで各要素の間隔を取っている。
image.png

画像をstackをまとめているstack

・AlignmentをTopにすることで文章が長くなっても画像が一番上に来るようにした。
・画像とstackの幅の比率を1:2に設定した。
・DiscributuonをFill Prportionallyにすることで画像の比率を崩さないようにした。
image.png

星の画像をまとめているstack

・DiscributuonをEqual Spacingにしたが、他のどの設定でも良かった。

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

image.png

widthを等しくする制約を消してstackViewの設定だけで実装しようとした時のレイアウト。
image.png

犬ラベルと説明textViewをまとめているstack

・Alignment、Discributuon共にFillにして横幅いっぱいに表示するようにした。
・textViewのscrolling Enabledをチェックマークを外して全文表示するようにしている。
image.png

コンテンツの高さに合わせてスクロールするView

画面を横にした時にコンテンツの高さだけスクロールできるViewを作る。
image.png

Scroll View の中に View を設置し中に部品を置く。
image.png

部品だけでViewの高さを決定するように配置を行う。
image.png

Viewの制約を追加する。

FrameLayoutGuideに対して、幅を等しくする制約を与える。
image.png

ContentLayoutGuideの上下左右と等しくする制約を設定して完成。
image.png

現在の仕様ではキーボードを表示した時に一番下のtextFieldが隠れてしまう。
image.png

キーボードを表示した時に上にスクロールしてずらす処理を実装する。
image.png

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

TableViewCellクラスを作成してxibファイルと紐づける。
image.png

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

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

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で下記の2行を追加

    tableView.delegate = self
    tableView.dataSource = self
5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3