この記事の目的
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です。
基本編
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。もう使いません。
次は、プロジェクトファイルの設定です。
プロジェクトファイル -> TARGETS -> General
とたどると、真ん中あたりに Main Interface
という欄があるはずです。これを、空欄にします。
空欄にしないでそのままにしておくと、先ほど削除した Main.storyboard
を参照してしまい、エラーが発生します。
余計なものを消したら、必要なものを追加しましょう。
起動して始めに表示する UIViewController
は、 AppDelegate.swift
で設定、実装します。 application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
メソッドに、以下のように3行追加してみましょう。
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を指定することができました。
わかりやすくするため、背景色を変えて確認してみましょう。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 背景色を変更
self.view.backgroundColor = UIColor.green
}
}
これで、Run!(⌘ + Rのショートカットが楽ですね)
こんな感じになれば成功です!
2. UIKitパーツを設置する
さて、次にUIKitパーツをAutoLayoutを使って配置してみましょう。
試しに、 UILabel
を1つ設置してみましょう。
SnapKitをimportして、UILabelをプロパティとして宣言。viewDidLoad
メソッドの中で、UILabelに文字を設定したりレイアウトしたりします。
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 してみましょう。
画像のようになれば成功です!
3. 画面遷移する
あとは、画面遷移です。まず、先ほどのViewControllerに、画面遷移用のボタンを配置してみましょう。
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
をつくってみます。
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です。
//前略
@objc func buttonDidTap(_ sender: UIButton) {
let secondViewController = SecondViewController()
self.present(secondViewController, animated: true, completion: nil)
}
SecondViewControllerをインスタンス化して、present
メソッドの引数に指定するだけです。
それでは、Runしてみましょう!
以上のようになったら成功です!
これであなたはコードで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
}()
let
が var
になったことに注意してください。これで、エラーが解消できます。
lazy
で宣言されたプロパティ(lazy stored property)はクラスや構造体の初期化時には初期化されず、アクセスされたときに初めて初期化されます。結果、 self
、すなわち ViewController
がロードされた直後にこのプロパティが初期化されるので、 self
が利用できる、というわけです。
今回の addTarget
に限らず、 delegate
や datasource
を指定するときにも利用できるので、使用頻度はそれなりに高いです。覚えておくと良いでしょう。
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のカスタムクラスをつくる場合、初期化は以下のようにします。
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
は、カスタムクラスをつくって実装します。
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実装が意外とカンタンだな、と思っていただければこの記事は成功です。
「こんな書き方の方がいいよ!」とか、「この辺がもっと知りたいよ!」とかあれば、コメントしてくれれば嬉しいです。
それでは、ご武運を!