[Xcode/Swift] Xib職人だった自分だけど今更コードレイアウト(SnapKitとか)にハマった

はじめに

SwiftUIがかなりメジャーになってきた昨今ですが、現場ではまだUIKitをゴリゴリ使う日々。

基本的にUI構築は、StoryboardとXIBを使ってやっている、そして一部にNSLayoutConstraitとかSnapkitという感じ。

この前のiOSDC2025に参加して、いろんな企業の開発の裏側を聞いているとやはりまだUIKitも現役で稼働してるところも多く、そして何よりUI構築もほぼコードベースで管理しているという企業もちらほらあった。

なので今一度GUIベースの構築だけじゃなくてコードベースでのUIKit実装も学び直してみようと思ったというきっかけがこの記事になります。

対象読者:

  • UIKit開発してて、普段はStoryboardやXib使ってる
  • Auto LayoutはGUIで組む派。コードはあんまり触ってない
  • SnapKitって名前は聞いたことあるけど、触ったことはない or 軽く触っただけ
  • UIKitなのにUIをコードベース実装、、、?って思ってる人

つまりUIKit初心者 or 普段GUIベースだけどコードベースも知っておくかって人が対象。

最終的に得られること:

  • SnapKitの基本と使いどころが完全に理解できてる
  • コードでUI組む=難しい」の感覚は消える (はず)
  • ここはGUI、ここはコードベースにするかって柔軟に実装目標を考えられるようになる

それでは一個ずつ復習していきましょう。


UIKit開発のスタイル比較

UIKitで画面を作成するとなった時に、考えられるのは以下の2つ。

GUIベース(Storyboard / Xib)or コードベース(NSLayoutConstraint / SnapKit)

良し悪しというよりかは、両方の特性を理解してその状況に応じてどっちを使うか柔軟に考えられるようになれればOK。

GUIベース(Storyboard / Xib)

メリット:

  • 視覚的に作れる:UIを直感的に配置できる。初心者でもとっつきやすい。
  • レイアウト確認が早い:シミュレーターで確認せずとも、Xcode上でだいたいの形が見える。
  • 学習コストが低い:Auto Layoutの基礎がわかればすぐ使える。

デメリット:

  • Gitコンフリクトが地獄:Storyboards/XibsはXML形式だから、複数人が編集すると高確率でコンフリクト。(XIBは割と控えめな気がする)
  • 保守しにくい:ファイルが分断されてて、どの画面がどのコードと紐づいてるか分かりづらい。
  • 動的なレイアウト変更が面倒:サイズ変更やアニメーションに弱い。結局コード書く羽目になる。

“あれ、Storyboardコンフリクトしてね?”, “誰かここConstraintいじった?”とかになりがちな面もある。

つまり手軽ではあるが、大規模 & 複数人開発では破綻する可能性も秘めているということを理解しておくとGOOD。


コードベース(NSLayoutConstraint / SnapKit)

メリット:

  • 変更・保守がしやすい:コードだから、Git管理も楽だし誰が何を変えたかも明確。
  • 共通View・部品の再利用がしやすい:Viewごとにロジックを分けて、綺麗に管理できる。
  • 動的レイアウトが得意:画面サイズに応じたConstraintの切り替え、アニメーションが簡単。

デメリット:

  • 学習コスト高め(特にNSLayoutConstraint):Anchorを1個1個書くのがだるい、読みにくい。
  • 初見では見た目がわかりにくい:GUIみたいに「見たままレイアウト」できないので想像力必要。
  • ⇧に関してはUIKitでもPreview使えるようになってるのでそこまで問題ではなくわなっている

学習コスト高いとは言われつつ、AutoLayoutの概念をある程度理解していればそこまで苦労せずに慣れるものなので、そこまで恐れずにまずは色々と試してみるのがGOODだと感じる。

※ UIKItでのPreview実装方法は以下を参考に

【Xcode/Swift】UIKitでもプレビューが使える件

NSLayoutConstraint vs SnapKit の違い

どちらもコードベースの実装、だけど色々内部の特徴は違ってくる。

NSLayoutConstraint = Apple純正 / SnapKit = サードパーティ製と理解しておけばOK、その上で以下の特徴。

項目NSLayoutConstraintSnapKit
書きやすさ冗長スッキリ
学習コスト高め低め
SwiftUI的な書き味遠い近い
公式サポートApple公式サードパーティ(でも安定)
アニメーション対応できるけど面倒.updateConstraints で簡単

SnapKitは、「GUIより柔軟で、ネイティブより書きやすい」っていう、ちょうどいいポジション。


結論:GUIかコードかじゃなくて「どう使い分けるか」

  • 画面が複雑でデザインの変更が多い → コードベース(SnapKit)一択
  • シンプルな画面で早く形にしたい → StoryboardでもOK
  • チーム開発でのメンテを考える → コードベースが圧倒的に楽
  • レイアウトの細かい制御が必要 → SnapKit最強

次は実際に同じ画面をStoryboard, NSLayoutConstrait, Snapkitでそれぞれ実装して肌感覚でどう違うかみていきましょう。


同じログインUIを3パターンで作ってみる

一般的なログイン画面をそれぞれのパターンで作って、どんな感じか見てみるというチャプター。

レイアウトパターン:

  • 上から順に縦に並べる
  • 水平は両端40ptマージン
  • 各要素の縦間隔は40pt
  • 高さ:TextField = 44、Button = 48
    全体は safeAreaLayoutGuide.top から80pt下に開始
  • 画面色はLightGray

これで作っていきましょう。

Storyboard

import UIKit

final class LoginViewController: UIViewController {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var idTextField: UITextField!
    @IBOutlet weak var passwordTextField: UITextField!
    @IBOutlet weak var loginButton: UIButton!

    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

.storyboardファイルに、コンポーネントをペタペタと貼り付けて、AutoLayoutを設定するのが基本。

ViewControllerとの接続は@IBOutlet、これが正しく接続されていないとスタイルだったりアクションが正常に処理されないという注意点がある。

特徴:

  • 見た目で調整しやすいけど、コード量よりもクリック作業多め
  • たまにクッソ重たくなる (個人的な恨み)
  • レイアウト崩れたとき原因追いにくい
  • コンフリクト地獄発生しやすい

NSConstraint

final class ViewController: UIViewController {

    private let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "ログイン"
        label.textColor = .black
        label.font = .systemFont(ofSize: 24, weight: .bold)
        label.textAlignment = .center
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()

    private let idTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "ID"
        textField.borderStyle = .roundedRect
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "パスワード"
        textField.isSecureTextEntry = true
        textField.borderStyle = .roundedRect
        textField.translatesAutoresizingMaskIntoConstraints = false
        return textField
    }()

    private let loginButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("ログイン", for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
        button.setTitleColor(.white, for: .normal) 
        button.backgroundColor = .systemBlue
        button.translatesAutoresizingMaskIntoConstraints = false
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .lightGray
        setupConstraints()
    }

    private func setupConstraints() {
        [titleLabel, idTextField, passwordTextField, loginButton].forEach {
            view.addSubview($0)
        }

        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 80),
            titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
            titleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40),

            idTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 40),
            idTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
            idTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40),
            idTextField.heightAnchor.constraint(equalToConstant: 44),

            passwordTextField.topAnchor.constraint(equalTo: idTextField.bottomAnchor, constant: 40),
            passwordTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
            passwordTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40),
            passwordTextField.heightAnchor.constraint(equalToConstant: 44),

            loginButton.topAnchor.constraint(equalTo: passwordTextField.bottomAnchor, constant: 40),
            loginButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40),
            loginButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40),
            loginButton.heightAnchor.constraint(equalToConstant: 48)
        ])
    }
}

比較なので、Constraint設定もベタ書きにしていますが、左右のConstraintは共通なら、
passwordTextField.leadingAnchor.constraint(equalTo: idTextField.leadingAnchor),とかにも出来ます、デザイン変更があってもidTextFieldのConstraintを変えるだけで済むので本番はこちらを採用するとGOOD。

特徴:

  • 完全コード。自由度高いけど、書く量多い
  • translatesAutoresizingMaskIntoConstraints = false を毎回書くのが地味に面倒
  • 保守性はStoryboardより断然マシ

SnapKit

final class ViewController: UIViewController {

    private let titleLabel: UILabel = {
        let label = UILabel()
        label.text = "ログイン"
        label.textColor = .black
        label.font = .systemFont(ofSize: 24, weight: .bold)
        label.textAlignment = .center
        return label
    }()

    private let idTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "ID"
        textField.borderStyle = .roundedRect
        return textField
    }()

    private let passwordTextField: UITextField = {
        let textField = UITextField()
        textField.placeholder = "パスワード"
        textField.isSecureTextEntry = true
        textField.borderStyle = .roundedRect
        return textField
    }()

    private let loginButton: UIButton = {
        let button = UIButton(type: .system)
        button.setTitle("ログイン", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.titleLabel?.font = .systemFont(ofSize: 20, weight: .bold)
        button.backgroundColor = .systemBlue
        return button
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .lightGray
        setupConstraints()
    }

    private func setupConstraints() {
        [titleLabel, idTextField, passwordTextField, loginButton].forEach {
            view.addSubview($0)
        }

        titleLabel.snp.makeConstraints { make in
            make.top.equalTo(view.safeAreaLayoutGuide).offset(80)
            make.leading.trailing.equalToSuperview().inset(40)
        }

        idTextField.snp.makeConstraints { make in
            make.top.equalTo(titleLabel.snp.bottom).offset(40)
            make.leading.trailing.equalToSuperview().inset(40)
            make.height.equalTo(44)
        }

        passwordTextField.snp.makeConstraints { make in
            make.top.equalTo(idTextField.snp.bottom).offset(40)
            make.leading.trailing.equalToSuperview().inset(40)
            make.height.equalTo(44)
        }

        loginButton.snp.makeConstraints { make in
            make.top.equalTo(passwordTextField.snp.bottom).offset(40)
            make.leading.trailing.equalToSuperview().inset(40)
            make.height.equalTo(48)
        }
    }
}

特徴:

  • スッキリしていて、一目で構造が読める
  • translatesAutoresizingMaskIntoConstraints 不要
  • inset が便利(マイナス考えなくてOK)
  • make.leading.trailing.equalToSuperview().inset(40)のように、まとめて書ける

比較まとめ

比較項目StoryboardNSLayoutConstraintSnapKit
視覚的に作れる✖️✖️
書きやすさ△(GUI操作)✖️(冗長)◎(宣言的)
保守性✖️(複雑化しやすい)
Git管理のしやすさ✖️
動的レイアウト対応
学習コスト低〜中

個人的に今後コードベースでUIKItやっていきたいとなったのであれば、基本はSnapkitを優先で使う感じになりそうかなと感じました。

せっかくなので最後にSnapkitの便利な特徴をまとめて頭の片隅に入れておきましょう。


SnapKitの便利セット

基本的な概念・使い方

1. snp.makeConstraints

一番基本のやつ。制約を新規で設定する。

view.snp.makeConstraints { make in
    make.top.equalToSuperview().offset(16)
    make.leading.trailing.equalToSuperview().inset(24)
    make.height.equalTo(50)
}

2. equalTo の使い方

  • equalToSuperview() → superview に対して
  • equalTo(otherView.snp.xxx) → 他のViewのAnchorに対して
  • offset(x) / inset(x) → 距離の調整(左右で符号意識不要)

3. updateConstraints

既存の制約を動的に変更したいとき

myView.snp.updateConstraints { make in
    make.height.equalTo(100)
}

4. remakeConstraints

全ての制約を一旦リセットして再設定したいとき

myView.snp.remakeConstraints { make in
    make.center.equalToSuperview()
    make.size.equalTo(200)
}

便利Tips・パターン集

.inset() vs .offset()

属性.inset(16).offset(16)
使いどころ左右・上下の「内側余白」指定Anchorからの「相対距離」指定
使用例make.leading.trailing.equalToSuperview().inset(20)make.top.equalTo(label.snp.bottom).offset(16)

基本は insetrelativeなときだけoffset使うって覚えておけばOK。


まとめて書けるやつ

top, bottom, leading, trailing を一括設定。

make.edges.equalToSuperview().inset(16)

レスポンシブなレイアウト

画面サイズに応じてレイアウトできる

make.width.equalToSuperview().multipliedBy(0.8)

サイズ固定

make.size.equalTo(CGSize(width: 100, height: 44))

UIStackViewとの組み合わせ

SnapKit + StackView は名コンビ、使えるところには積極的に使うとGOOD

let stack = UIStackView(arrangedSubviews: [label, textField, button])
stack.axis = .vertical
stack.spacing = 24
view.addSubview(stack)

stack.snp.makeConstraints { make in
    make.leading.trailing.equalToSuperview().inset(24)
    make.centerY.equalToSuperview()
}

まとめ:SnapKitはなんだかんだ便利

スキルできるようになると強い
.makeConstraints の基本UI組めるようになる
.update, .remakeアニメーションや状態変化に対応
.inset.offset の違いレイアウトの意図が伝わるコードが書ける
StackViewとの合わせ技レスポンシブ&柔軟なUIが構築できる

おわり

なんだかんだ、SnapkitになれるとUIKitもコードベース実装が保守性面でもチーム開発面でもかなり便利って結論。

GUI開発から抜け出したい人、両方学び直したい人にとって、SnapKitは間違いなくGOODな選択。

ぜひこの便利セット持って、次のUI画面から色々と試してみてください。。