36
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

iOSとコードベースレイアウト

Last updated at Posted at 2019-01-31

はじめに

本記事はMobileAct TOKYO #5で発表した以下の内容をまとめたものになります。
※スライド版はこちらです。

  • Storyboard、XIB、コードベースといったiOSにおけるレイアウト手法の比較
  • コードベースレイアウトの6つのTips
  • Githubのサンプルコード

比較表

比較項目 Storyboard XIB コードベース
➡️ Segueが使える
👀 GUIで見てわかる
📱 画面サイズの分岐が簡単
🔎 静的チェック
💉コンストラクタDI
♻️ 再利用性
💥 コンフリクトしにくい
🛂 レビューしやすさ

StoryboardとXIBの特徴

良いところ

  • GUIで操作が直感的
  • Storyboardの場合はSegueが使える
  • 複数の画面サイズでの見え方がわかる
  • Size Classesによる画面サイズによる分岐がしやすい
  • 静的な制約チェック

イマイチなところ

  • 部分的にコピペしたり、カスタムクラス化するなど再利用・変更がしづらい
  • コンフリクトの解消が難しい
  • XMLなのでコードレビューが難しい

コードベースレイアウトの特徴

良いところ

  • コードの再利用がしやすい
  • コピペやコード置換などでスピーディに修正が可能
  • コンフリクトの解消がしやすい
  • レビューがしやすい

イマイチなところ

  • UIKitのオブジェクトのプロパティの知識やライフサイクルに関する知識が必要
  • 実行するまで見え方は想像でしかわからない

コンストラクタインジェクションの違い

Storyboardでは初期化処理がUIStoryboardクラスによって隠蔽されるため、コンストラクタインジェクションを行うことができません。

let storyboard = UIStoryboard(name: "ViewController", bundle: nil)
let storyboardViewController = storyboard.instantiateInitialViewController()

逆にXIB、コードベースレイアウトの場合は以下のように記述することでコンストラクタインジェクションが可能です。

class ViewController: UIViewController {
    let dependency: Dependency
    init(dependency: Dependency) { 
        self.dependency = dependency
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

もちろんStoryboardでもプロパティインジェクションは可能ですが、プロパティをvarやOptional型にする必要が出てくるため、可能ならば避けたいです。

コードベースレイアウトの6つのTips

  1. Initialization Closure
  2. Then
  3. カスタムクラス
  4. lazy var
  5. SnapKit / PureLayout
  6. UIStackView

1. Initialization Closure

コードベースレイアウトを安直に行なった場合、プロパティの設定コードやレイアウトコードが多くなり、コードが汚くなりがちです。

class ViewController: UIViewController {
    let priceLabel = UILabel()
    let imageView = UIImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        priceLabel.numberOfLines = 2
        priceLabel.textColor = .red
        priceLabel.font = .boldSystemFont(ofSize: 14)
        
        imageView.contentMode = .scaleAspectFill
        imageView.clipsToBounds = true
        imageView.layer.cornerRadius = 4
        // addSubview, AutoLayout...
    }
}

Initialization Closure(初期化クロージャ)によってプロパティの初期化処理をまとめることができます。

class ViewController: UIViewController {
    let priceLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 2
        label.textColor = .red
        label.font = .boldSystemFont(ofSize: 14)
        return label
    }()
    let imageView: UIImageView = {
        let view = UIImageView()
        view.contentMode = .scaleAspectFill
        view.clipsToBounds = true
        view.layer.cornerRadius = 4
        return view
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // addSubview, AutoLayout...
    }
}

2. Then

viewDidLoadのコードは減りましたが、まだ少し冗長なコードになっています。

class ViewController: UIViewController {
    let priceLabel: UILabel = {                   // 型アノテーション
        let label = UILabel()                     // ローカルスコープでの命名
        label.numberOfLines = 2                   // label
        label.textColor = .red                    // label
        label.font = .boldSystemFont(ofSize: 14)  // label
        return label                              // 生成したインスタンスのreturn
    }()
    let imageView: UIImageView = {                // 型アノテーション
        let view = UIImageView()                  // ローカルスコープでの命名
        view.contentMode = .scaleAspectFill       // view
        view.clipsToBounds = true                 // view
        view.layer.cornerRadius = 4               // view
        return view                               // 生成したインスタンスのreturn       
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // addSubview, AutoLayout...
    }
}

オープンソースライブラリのdevxoul/Thenを利用することで、よりシンプルに書くことができます。

class ViewController: UIViewController {
    let priceLabel = UILabel().then {
        $0.numberOfLines = 2
        $0.textColor = .red
        $0.font = .boldSystemFont(ofSize: 14)
    }
    let imageView = UIImageView().then {
        $0.contentMode = .scaleAspectFill
        $0.clipsToBounds = true
        $0.layer.cornerRadius = 4
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // addSubview, AutoLayout...
    }
}

Then自体は1ファイルのとてもミニマムなライブラリでthen関数の実装は以下のようになっています。

public protocol Then {}
extension Then where Self: AnyObject {
  public func then(_ block: (Self) throws -> Void) rethrows -> Self {
    try block(self)
    return self
  }
}

3. カスタムクラス

使い回すViewであればUIViewを継承したカスタムクラスを定義することで再利用性を高め、よりシンプルに記述できます。

class ViewController: UIViewController {
    let priceLabel = PriceLabel()
    let imageView = ItemImageView()
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

多くのUI部品を表示する画面でもカスタムクラス化によってクラスを小さくし、分割統治をすることでより保守性を高められるかと思います。

4. lazy var

プロパティの初期化時にselfや他のプロパティを参照したい場合があります。

private let captureButton = CaptureButton().then {
    $0.onTapped = { [weak self] in
        self?.camera.capture()
    }
}

プロパティの初期化時にはselfはまだ初期化されていないため、コンパイルエラーとなります。

private let captureButton = CaptureButton().then {
    $0.onTapped = { [weak self] in // コンパイルエラー
        self?.camera.capture()     // コンパイルエラー
    }
}

そのような場合、lazy varで定義することで初期化処理をプロパティの最初の参照時に遅延できるため、selfを利用することができます。

private lazy var captureButton = CaptureButton().then {
    $0.onTapped = { [weak self] in
        self?.camera.capture()    // OK
    }
}

5. SnapKit / PureLayout

次はコードによってAutoLayoutを記述します。

iOS9から利用可能なNSLayoutAnchorで記述すると以下のようになります。

view.addSubview(button)
button.leadingAnchor.constraintEqualToAnchor(view.leadingAnchor).active = true
button.trailingAnchor.constraintEqualToAnchor(view.trailingAnchor).active = true
button.topAnchor.constraintEqualToAnchor(view.topAnchor).active = true
button.bottomAnchor.constraintEqualToAnchor(view.bottomAnchor).active = true

記述量が多くてとても大変です🤮

オープンソースライブラリのSnapKitPureLayoutを利用することで、コードはかなりシンプルになります。

view.addSubview(button)
button.snp.makeConstraints { $0.edges.equalToSuperview() }

その他の記述例はこのような感じです。

//  読みやすい
button.snp.makeConstraints {
    $0.center.equalToSuperview()                   // 親ビューの中心に
    $0.size.equalTo(CGSize(width: 64, height: 64)) // サイズ指定
}

// Safe Areaにもレイアウトを貼りやすい
tableView.snp.makeConstraints {
    $0.top.bottom.equalTo(view.safeAreaLayoutGuide) // 上下はSafe Areaに
    $0.leading.trailing.equalToSuperview()          // 左右は親ビューに
}

6. UIStackView

UIStackViewクラスはViewを縦横に積むことができるレイアウト要素を持ったViewのため、UIStackViewを活用することで制約がシンプルになります。

stackView.addArrangedSubview(label)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(button)

label.snp.makeConstraints { $0.height.equalTo(20) } 
button.snp.makeConstraints { $0.height.equalTo(60) } 

UIStackViewをネストさせることもできますが、階層構造が深くなるとわかりにくくなるため、個人的な目安としては、1クラスにつき2階層程度にし、階層が深くなった場合は別クラスに切り出すと良いかと思います。

まとめ

  • コードベースレイアウトでもTipsを使えば簡潔かつスピーディに書ける
  • どの手法もできるようにしておき、使い分けたい

例)

  • シンプルなレイアウト要件にはコードベース
  • 複雑なレイアウト要件にはXIB
  • Segueを利用したい場合にはStoryBoard

サンプルコード

Storyboard、 XIB、 コードベースレイアウトのサンプルコードです。
https://github.com/shtnkgm/iOSUILayoutMethods

ViewController(画面)ごとに以下の異なる手法でレイアウトを実装しました。

  • Storyboard
  • XIB
  • コードベース(Thenを利用した初期化)
  • NSLayoutAnchorを利用したAutoLayout
  • PureLayoutを利用したAutoLayout
  • SnapKitを利用したAutoLayout
36
23
1

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
36
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?