Storyboard派がコードでUIを実装するためのチュートリアル

# この記事の目的
**StoryboardでUIを実装してきたiOSエンジニアが、コードだけでUIを実装できるようになる**のがゴールです。

初心者向け。

## 対象
- コードでUIを実装する方法を学びたい方
- Storyboardかコードか悩んでいる方

# なぜ、Storyboard（Interface Builder）を使わないのか
一言で言ってしまえば、**手間が減る** からです。特に、チーム開発では顕著です。

## コードでUIを実装するメリット

- コードレビューがカンタン、差分がわかりやすい
- プルリクエスト、マージしようとしたときにコンフリクト（競合）が起きにくい
- パーツやUIViewControllerの再利用、継承がカンタン
- 動作が軽い（Interface Builderが重い）
- 実装がコードに集約される（読みやすい）

## コードでUIを実装するデメリット

- iOSアプリ開発の入門はStoryboard前提のものが大半のため、学習コストがかかる
- レイアウトの確認に時間がかかる
- iOSエンジニア以外がレイアウトを確認したり、微調整するのに困難がある

ただ、**iPhoneのサイズが多様化** してきたために、シンプルなAutoLayoutの制約だけではレイアウトの設定が難しくなってきたことから、これらのデメリットが薄れてきているな、というのが個人的な見解です。

Storyboard（Interface Builder）を利用しない実装の良いところについては、エウレカさんの以下の記事がとっても参考になります。

[Interface Builderに依存しないiOS開発のススメ](https://medium.com/eureka-engineering/interface-builderに依存しないios開発のススメ-13811b9236d5)

# この記事の前提
- Xcode 10.1
- Swift 4.2
- AutoLayout
- SnapKit（AutoLayoutがコードで書きやすくなるライブラリ）を使用

# 前提知識
## AutoLayout
もはや、iOSアプリのレイアウトを攻略するには必須知識です。
各View（パーツ）との位置関係やサイズを指定することで、大きさの違うiPhoneでもいい感じにパーツを配置できる仕組みです。

以前はバリエーションの少なかったiPhoneサイズですが、最新のiOS（iOS 12）に対応しているものだけでも、サイズやアスペクト比（縦横比）が違う以下の5種類が存在します。

iPhone SE、iPhone 8、 iPhone 8 Plus、iPhone XS、 iPhone XR、 iPhone XS Max

iPadは以下の5種類です。

（iPhone 4s）、 iPad Pro 9.7、 iPad Pro 10.5、 iPad Pro 11、 iPad Pro 12.9、 iPad Pro 12.9（第三世代）

これだけの種類があると、もはやAutoLayoutを使わずにレイアウトを実装するのは厳しいと言わざるを得ません。

というわけで、この記事では**AutoLayoutの利用を前提**にします。

## ライブラリの導入方法
今回は、AutoLayoutをコードで記述するため、 `SnapKit` というライブラリを利用します。 

なお、 `CocoaPods` `Carthage` `手動` いずれでも問題ありません。

# コードでiOSのレイアウトを実装するチュートリアル

お待たせしました。ここからが本番です。

テスト用に、適当に新規プロジェクトでも作って始めてみましょう。

`Single View App` でOKです。

<img width="1512" alt="SingleViewApp.png" src="https://qiita-image-store.s3.amazonaws.com/0/66650/62bc3a31-6096-5434-b8a3-ffe521c80297.png">

## 基本編
### 1. SnapKitをインストールする

まずは事前準備として、AutoLayoutをコードでカンタンに書くために、 `SnapKit` をインストールしましょう。 前述の通り、`CocoaPods` でも、 `Carthage` でも、 `手動` でもOK。好きな方法で導入しましょう。

詳しくは、[SnapKitのページ](https://github.com/SnapKit/SnapKit)を参考にしてください。


#### CocoaPodsの場合
```
pod 'SnapKit', '~> 4.0.0'
```

#### Carthageの場合
```
github "SnapKit/SnapKit" ~> 4.0.0
```

### 2. 起動時に表示するUIViewControllerを指定する

いきなりですが、`Main.storyboard`を消してしまいましょう！

`Move to Trash` でOK。もう使いません。

<img width="658" alt="MoveToTrash.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/66650/504d2bed-5d13-2175-dfd4-060ef66722e8.jpeg">

次は、プロジェクトファイルの設定です。

`プロジェクトファイル -> TARGETS -> General` とたどると、真ん中あたりに `Main Interface` という欄があるはずです。これを、**空欄**にします。

空欄にしないでそのままにしておくと、先ほど削除した `Main.storyboard` を参照してしまい、エラーが発生します。

<img width="1512" alt="Main.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/66650/dfc692fb-3360-943a-7293-e688276a3aa2.jpeg">

余計なものを消したら、必要なものを追加しましょう。
起動して始めに表示する `UIViewController` は、 `AppDelegate.swift` で設定、実装します。 `application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?)` メソッドに、以下のように3行追加してみましょう。

```AppDelegate.swift
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        /* 最初に表示するUIViewControllerを指定する */
        // windowをスクリーンサイズに合わせて生成
        window = UIWindow(frame: UIScreen.main.bounds)
        // ViewControllerをインスタンス化、windowのrootに設定する
        window!.rootViewController = ViewController()
        // 表示する
        window!.makeKeyAndVisible()
        
        return true
    }

//（以下略）
}
```

これで、起動後に表示するUIViewControllerを指定することができました。

わかりやすくするため、背景色を変えて確認してみましょう。

```ViewController.swift
import UIKit

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 背景色を変更
        self.view.backgroundColor = UIColor.green
    }
}
```

これで、**Run**！（⌘ + Rのショートカットが楽ですね）

<img width="584" alt="スクリーンショット 2018-12-04 8.42.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/66650/3d957fa4-8858-d3fb-3c94-d7a2af26e03f.png">

こんな感じになれば成功です！

### 2. UIKitパーツを設置する

さて、次にUIKitパーツをAutoLayoutを使って配置してみましょう。

試しに、 `UILabel` を１つ設置してみましょう。
SnapKitをimportして、UILabelをプロパティとして宣言。`viewDidLoad` メソッドの中で、UILabelに文字を設定したりレイアウトしたりします。

```ViewController.swift
import UIKit
import SnapKit

class ViewController: UIViewController {
    
    // MARK: Views
    let label = UILabel()
    
    // MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 背景色を変更
        self.view.backgroundColor = UIColor.green
        
        /* ラベルを配置 */
        // ラベルに文字列を設定
        self.label.text = "平成最後のアドベントカレンダー！"
        // ラベルを設置
        self.view.addSubview(self.label)
        // ラベルの位置をSnapKit（AutoLayoutで指定）
        self.label.snp.makeConstraints { (make) in
            make.center.equalToSuperview() // 中心を親Viewに合わせる
        }
    }
}
```

UILabelをプロパティとして宣言し、viewDidLoadメソッドでラベルの配置をしています。

写経ができたら **Run** してみましょう。

<img width="584" alt="スクリーンショット 2018-12-04 8.48.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/66650/f6165997-2b38-8d2f-b298-18b20579efdc.png">

画像のようになれば成功です！

### 3. 画面遷移する

あとは、画面遷移です。まず、先ほどのViewControllerに、画面遷移用のボタンを配置してみましょう。

```ViewController.swift
import UIKit
import SnapKit

class ViewController: UIViewController {
    
    // MARK: Views
    let label = UILabel()
    let button = UIButton() // 追加
    
    // MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 背景色を変更
        self.view.backgroundColor = UIColor.green
        
        /* ラベルを配置 */
        // ラベルに文字列を設定
        self.label.text = "平成最後のアドベントカレンダー！"
        // ラベルを設置
        self.view.addSubview(self.label)
        // ラベルの位置をSnapKit（AutoLayoutで指定）
        self.label.snp.makeConstraints { (make) in
            make.center.equalToSuperview() // 中心を親Viewに合わせる
        }
        
        // 追加
        /* ボタンを配置 */
        self.view.addSubview(self.button)
        self.button.setTitle("Next", for: .normal)
        self.button.addTarget(self, action: #selector(self.buttonDidTap(_:)), for: .touchUpInside)
        self.button.snp.makeConstraints { (make) in
            make.centerX.equalToSuperview() // X軸中心を親Viewに合わせる
            make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).inset(100) //下から100ポイント上に配置
        }
    }
    
    // ボタンをタップしたときに呼ばれる
    @objc func buttonDidTap(_ sender: UIButton) {
    }
}
```

以下の部分が、Storyboardを利用したときでいう、`IBAction` の部分。ボタンがタップされたときに呼ばれるメソッドです。

```Swift
@objc func buttonDidTap(_ sender: UIButton) {
}
```

そして、このメソッドと、ボタンのタップアクションを結びつけるのが以下の部分です。

```Swift
self.button.addTarget(self, action: #selector(self.buttonDidTap(_:)), for: .touchUpInside)
```

ボタンの設置ができたので、遷移先のUIViewControllerをつくって、実際に遷移してみましょう。
再利用のしやすい、「コードでのUI実装」です。せっかくなので、ここでは先ほどのViewControllerを継承して、`SecondViewController` をつくってみます。

```SecondViewController.swift
import UIKit
import SnapKit

// ViewControllerを継承する
class SecondViewController: ViewController {
    
    // MARK: Views
    
    // MARK: Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 背景色を変更
        self.view.backgroundColor = UIColor.red
        // ラベルの文字を変更
        self.label.text = "みんなは何か書いた？"
        // ボタンの文字を変更
        self.button.setTitle("Back", for: .normal)
    }
    
    @objc override func buttonDidTap(_ sender: UIButton) {
        self.dismiss(animated: true, completion: nil)
    }
}
```

背景と、ラベルの文字、ボタンの文字を変えました。
また、以下の部分で、ボタンアクションを上書き（override）して変更。遷移前の画面に戻る実装をしています。

```Swift
@objc override func buttonDidTap(_ sender: UIButton) {
    self.dismiss(animated: true, completion: nil)
}
```

では、ViewController.swiftに戻って、ボタンをタップした後にSecondViewControllerに遷移できるようにしてみましょう。以下のように実装すればOKです。

```ViewController.swift
//前略
@objc func buttonDidTap(_ sender: UIButton) {
    let secondViewController = SecondViewController()
    self.present(secondViewController, animated: true, completion: nil)
}
```

SecondViewControllerをインスタンス化して、`present` メソッドの引数に指定するだけです。

それでは、**Run**してみましょう！


![画面遷移.gif](https://qiita-image-store.s3.amazonaws.com/0/66650/6541a80b-79e0-be22-40fe-f900b4c1b186.gif)

以上のようになったら成功です！

これであなたはコードでiOSアプリのUIを実装することができるようになりました！おめでとうございます！

## 応用編

とはいえ、ここまでに行った実装だけでは、少々困ることがあるでしょう。

というわけで、いくつか実践的な手法をご紹介しますので、ご参考ください。

### SnapKitでのAutoLayout

参考になるページが日本語でもたくさんあるので、「SnapKit 使い方」とかで調べると良いです。

ただ、AutoLayoutを使いこなせていれば、基本は[公式ドキュメント](http://snapkit.io/docs/)で十分だと思います。ご参考ください。

なお、iPhone X系で重要になる `SafeArea` についても安心です。

本チュートリアルのボタンの配置でこっそり使っていた通り、以下のように記述できます。

```Swift
self.button.snp.makeConstraints { (make) in
    make.centerX.equalToSuperview() // X軸中心を親Viewに合わせる
    make.bottom.equalTo(self.view.safeAreaLayoutGuide.snp.bottom).inset(100) //下から100ポイント上に配置
}
```

### UIKitパーツの初期化をわかりやすくする

今回のチュートリアルでは、UILabelやUIButtonの設定をviewDidLoadメソッドの中で行いました。

パーツが少ないうちは良いのですが、多くなってくるとメソッドが肥大化してわかりにくくなるので、プロパティの宣言を以下のように記述するのがおすすめです。

```Swift
let label: UILabel = {
    let label = UILabel()
    label.text = "平成最後のアドベントカレンダー！"
    return label
}()
```

これで、UILabelの初期設定を、viewDidLoadに書かずに済むようになりました。

ちなみにこの書き方は、`Initialization Closure` と呼ばれます。

#### DelegateやaddTargetしたいとき

さて、ではボタンも同じように…といきたいところなのですが、このまま以下のように記述すると、エラーが出ます。

```Swift
let button: UIButton = {
    let button = UIButton()
    button.setTitle("Next", for: .normal)
    button.addTarget(self, action: #selector(self.buttonDidTap(_:)), for: .touchUpInside)
    return button
}()
```

エラーの内容は、 `Value of type '(ViewController) -> () -> (ViewController)' has no member 'buttonDidTap'`。

何かというと、ViewControllerを読み込む前に `self` を利用しようとしているのが原因です。

これを防ぐためのキーワードが `lazy` です。以下のように記述します。

```Swift
lazy var button: UIButton = {
    let button = UIButton()
    button.setTitle("Next", for: .normal)
    button.addTarget(self, action: #selector(self.buttonDidTap(_:)), for: .touchUpInside)
    return button
}()
```

`let` が `var` になったことに注意してください。これで、エラーが解消できます。

`lazy` で宣言されたプロパティ（lazy stored property）はクラスや構造体の初期化時には初期化されず、アクセスされたときに初めて初期化されます。結果、 `self`、すなわち `ViewController` がロードされた直後にこのプロパティが初期化されるので、 `self` が利用できる、というわけです。

今回の `addTarget` に限らず、 `delegate` や `datasource` を指定するときにも利用できるので、使用頻度はそれなりに高いです。覚えておくと良いでしょう。

### UITableViewおよびUITableViewCell

UITableViewをコードで宣言する場合は、↑の`lazy`を使って以下のようにするのが便利です。

```Swift
lazy var tableView: UITableView = {
    let tableView = UITableView()
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") //Cellを登録する
    tableView.delegate = self
    tableView.dataSource = self
    return tableView
}()
```

ポイントは、以下の１行で再利用するセルを指定するところ。StoryboardでUITableViewの上にCellを配置する代わりに、以下のようにします。

```Swift
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") //Cellを登録する
```

また、UITableViewCellのカスタムクラスをつくる場合、初期化は以下のようにします。

```CustomCell.swift
class CustomCell: UITableViewCell {

    let label = UILabel()

//(前略）
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        self.contentView.addSubview(self.label)
        // ラベルの位置をSnapKit（AutoLayout）で指定
        self.label.snp.makeConstraints { (make) in
            make.center.equalToSuperview() // 中心を親Viewに合わせる
        }
    }
}
```

UIViewControllerと異なり、`viewDidLoad` メソッドがないので、`init` （イニシャライザ）をoverrideしてレイアウトを指定します。

### UITabBarController

`UITabBarController` は、カスタムクラスをつくって実装します。

```CustomTabBarController.swift
import UIKit

class CustomTabBarController: UITabBarController {

    let firstVC = UIViewController()
    let secondVC = UIViewController()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //タブのアイコンやタイトルを指定
        firstVC.tabBarItem = UITabBarItem(title: "１つめ", image: nil, tag: 0)
        secondVC.tabBarItem = UITabBarItem(title: "2つめ", image: nil, tag: 1)

        //タブで表示するUIViewControllerを設定
        self.setViewControllers([firstVC, secondVC], animated: true)
    }
}
```
### UINavigationController

`UINavigationController` は、カスタムクラスをつくる必要もなく、カンタンです。
以下のように、呼び出したいところでrootとなるUIViewControllerを指定して初期化すれば良いだけです。

```Swift
let viewController = UIViewController()
let navigationController = UINavigationController(rootViewController: viewController)
```

#まとめ

いかがだったでしょうか。コードでのUI実装が意外とカンタンだな、と思っていただければこの記事は成功です。

「こんな書き方の方がいいよ！」とか、「この辺がもっと知りたいよ！」とかあれば、コメントしてくれれば嬉しいです。

それでは、ご武運を！