83
69

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.

Team サイゼリヤAdvent Calendar 2018

Day 5

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

Last updated at Posted at 2018-12-05

この記事の目的

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開発のススメ

この記事の前提

  • 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です。

SingleViewApp.png

基本編

1. SnapKitをインストールする

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

詳しくは、SnapKitのページを参考にしてください。

CocoaPodsの場合

pod 'SnapKit', '~> 4.0.0'

Carthageの場合

github "SnapKit/SnapKit" ~> 4.0.0

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

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

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

MoveToTrash.jpg

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

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

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

Main.jpg

余計なものを消したら、必要なものを追加しましょう。
起動して始めに表示する 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のショートカットが楽ですね)

スクリーンショット 2018-12-04 8.42.28.png

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

2. UIKitパーツを設置する

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

試しに、 UILabel を1つ設置してみましょう。
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 してみましょう。

スクリーンショット 2018-12-04 8.48.15.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 の部分。ボタンがタップされたときに呼ばれるメソッドです。

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

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

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)して変更。遷移前の画面に戻る実装をしています。

@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

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

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

応用編

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

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

SnapKitでのAutoLayout

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

ただ、AutoLayoutを使いこなせていれば、基本は公式ドキュメントで十分だと思います。ご参考ください。

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

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

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メソッドの中で行いました。

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

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

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

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

DelegateやaddTargetしたいとき

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

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 です。以下のように記述します。

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

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

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

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

UITableViewおよびUITableViewCell

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

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

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

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: "1つめ", image: nil, tag: 0)
        secondVC.tabBarItem = UITabBarItem(title: "2つめ", image: nil, tag: 1)

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

UINavigationController

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

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

#まとめ

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

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

それでは、ご武運を!

83
69
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
83
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?