Storyboardを使わないiOSアプリ開発
もう今はSwiftUIじゃない?って感じですが、僕の関わっているプロジェクトではまだUIKitを利用した開発をしているので、その記録として残しておきます。
なぜStoryboardを使わない?
基本的にはGithubでコードを管理していて、個人的にStoryboardのようなXMLで書かれたものを読むのは結構難しく感じます。あとはStoryboardのファイルを開くと特に変更してなくても構造が更新されていたりします。あとはStoryboardのファイルを開くときに遅いなどといった点もありました。
結局はアニメーションなど複雑な事をしようとするとコードで書くことが多かったのもあり、それなら全部コードで良くない?ってなったためStoryboardから脱却することにしました。
Storyboardを削除する
今回は既存プロジェクトではなく新規プロジェクトをStoryboardで立ち上げています。
初期のプロジェクトだと立ち上げ時に表示される画面はMain.storyboard
のView Controllerになるので、まずはMain.storyboard
をプロジェクトから削除します。ViewController.swift
は削除せずに後で使います。さて、削除した段階でビルドするとエラーになるので、いくつか設定をしていきます。
Main Interfaceを空白にする
TARGETS > ProjectName > General > Development Info
のMain Interfaceを空白にします。
Info.plistのStoryboard Nameの項目を削除
Info.plist
の中にあるStoryboard Nameというのを削除します。場所は Information Property List > Application Scene Manifest > Scene Configuration > Application Session Role > Item 0 > Storyboard Name
です。
これをマイナスボタンから削除します。
このタイミングですでにビルドは成功して起動ができるようになります。しかしまだ画面が真っ暗で何も表示されていないと思います。
SceneDelegate.swiftを編集(iOS13以降)
起動時に表示するViewControllerをSceneDelegate.swift
に記述します。SceneDelegateが利用できるのはiOS13以降なので、iOS12以前のアプリケーションでは利用できません。
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = ViewController.init()
self.window = window
window.makeKeyAndVisible()
}
}
表示したいViewControllerをイニシャライズしてwindowのrootViewControllerに渡してあげるとそのViewControllerが起動時の初期画面として表示されます。
AppDelegate.swiftを編集(iOS12以前)
iOS12以前はAppDelegateを利用します。
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = ViewController()
window?.makeKeyAndVisible()
return true
}
ビルドしてViewControllerが表示されているか確認する
ViewController.swiftにはまだコードを書いていない状態です。わかりやすく表示されているか確認するために背景を赤にします。
override func viewDidLoad() {
super.viewDidLoad(
view.backgroundColor = .red
}
Simulatorにビルドして背景が赤くなります。
「Hello world!」を表示してみる
テキストを表示するのでUILabelを使っていきます。
配置は基本的にはAutoLayoutを利用します。
まずはViewControllerにUILabelをストアドプロパティとして定義します。
let titleLabel: UILabel = {
let view = UILabel.init()
view.text = "Hello World!"
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
AutoLayoutをコードから利用するためにはautoresizingをoffにする必要があるので、必ずtranslatesAutoresizingMaskIntoConstraints
をfalseにしています。
次にViewControllerのViewに対してaddSubviewし、その後にAutoLayoutの制約を記述していきます。制約は特定のViewとViewの相対的な関係を定義するものになります。
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(titleLabel)
NSLayoutConstraint.activate([
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0)
])
}
上記はtitleLabel
とview
のX軸、Y軸の中心が同じになるということを表しています。
つまりはviewの中心に配置するという意味です。これを実行してみます。
中心に表示されました。Storyboardのときに比べてコードを書く量は増えますが、それなりにメリットはあるように思います。ただし、AutoLayoutが若干理解するのに難しいです。
- topAnchor
- bottomAnchor
- leadingAnchor
- trailingAnchor
- centerYAnchor
- centerXAnchor
これらを使って定義します。
AtomicDesign等で要素を分解して、パーツ、コンポーネントでUIViewを作っておくと、後はサクサク組み立てて埋め込んでいく、みたいな感じにできそうです。
画面遷移をする
Storyboardではボタンクリックをしたら画面遷移させる等もGUIでできていました。しかしコードオンリーになった今それを使うことができません。Storyboardにはidentifierを設定して、コード側でSegue遷移させる方法もありますが、Stringで管理しないと行けないはアプリケーションが大きくなるにつれ辛くなりそうなので使いたくはありませんでした。
- 下から出てくる画面遷移
- Navigationを使った画面遷移
まずは遷移させるために新しくViewControllerを作ります。クラス名は ModalViewController
にしました。特に意味はありません。
背景の色をわかりやすくGreenにしておきます。それ以外は特に変えていません。
import UIKit
class ModalViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .green
}
}
下から出てくる画面遷移
さてViewController -> ModalViewController
へと遷移させるため、ボタンを配置し、そのボタンがタップされたときに画面遷移するようにします。記述したコードは以下の様になっています。
import UIKit
class ViewController: UIViewController {
let titleLabel: UILabel = {
let view = UILabel.init()
view.text = "Hello World!"
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var button: UIButton = {
let view = UIButton.init()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBlue
view.setTitle("下から出てくるやつ", for: .normal)
view.addTarget(self, action: #selector(openModal(_:)), for: .touchDown)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(titleLabel)
view.addSubview(button)
NSLayoutConstraint.activate([
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
button.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
}
@objc func openModal(_ sender: UIButton) {
let modalViewController = ModalViewController.init()
present(modalViewController, animated: true, completion: nil)
}
}
まずひとつずつ見ていきます。
ボタン定義する
ボタンは押したときのアクションを追加したいので、レイジーストアドプロパティを利用しています。つまりviewDidLoad()
の中でbutton
が初めて呼ばれたタイミングに初めてインスタンスの生成がされることになります。
AutoLayoutは「Hello World!」と書かれたUILabelからY軸は10(pxというかはちょっとわからない)下に、X軸はviewの中心に配置することにしました。
ボタンが押されたときの処理を書く
下から画面を出すには present
を呼ぶ必要があります。documentを見てわかるように1つ目の引数に遷移させたいViewControllerを渡します。2つ目にアニメーションさせるかどうか、3つ目は遷移が終わったときのクロージャーを渡します。今回は別に処理させる必要がないのでnilを渡しています。
余談
下から遷移させるモダールを全画面にするためにはViewControllerのインスタンスのmodalPresentationStyle
に.fullScreen
をセットすると全画面になります。
let modalViewController = ModalViewController.init()
modalViewController.modalPresentationStyle = .fullScreen
present(modalViewController, animated: true, completion: nil)
Navigationを使った画面遷移
Navigationを使った今回の遷移は ViewController > FirstPageViewController > SecondPageViewController
にします。
まずはFirstPageViewControllerを作ります。
import UIKit
class FirstPageViewController: UIViewController {
lazy var button: UIButton = {
let view = UIButton.init()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBlue
view.setTitle("次のページへ", for: .normal)
view.addTarget(self, action: #selector(nextPage(_:)), for: .touchDown)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemPink
view.addSubview(button)
NSLayoutConstraint.activate([
button.centerYAnchor.constraint(equalTo: view.centerYAnchor),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
])
}
@objc func nextPage(_ sender: UIButton) {
}
}
後でnextPageの中身は記述します。FirstPageViewControllerはわかりやすくピンクにします。
FirstPageViewControllerからSecondPageViewControllerに遷移するためのボタンを配置しました。次にFirstPageViewControllerから遷移するSecondPageViewControllerを作ります。
import UIKit
class SecondPageViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .orange
}
}
SecondPageViewControllerはわかりやすくオレンジにします。
Navigationを使う場合はUINavigationViewControllerでラップする
さて、次にFirstPageViewControllerに遷移するためにViewControllerの中身を変更していきます。
import UIKit
class ViewController: UIViewController {
let titleLabel: UILabel = {
let view = UILabel.init()
view.text = "Hello World!"
view.backgroundColor = .red
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var button: UIButton = {
let view = UIButton.init()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBlue
view.setTitle("下から出てくるやつ", for: .normal)
view.addTarget(self, action: #selector(openModal(_:)), for: .touchDown)
return view
}()
// add
lazy var navButton: UIButton = {
let view = UIButton.init()
view.translatesAutoresizingMaskIntoConstraints = false
view.backgroundColor = .systemBlue
view.setTitle("ナビゲーション", for: .normal)
view.addTarget(self, action: #selector(openNavigation(_:)), for: .touchDown)
return view
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
view.addSubview(titleLabel)
view.addSubview(button)
view.addSubview(navButton) // add
NSLayoutConstraint.activate([
titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),
titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
button.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10),
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
navButton.topAnchor.constraint(equalTo: button.bottomAnchor, constant: 10), // add
navButton.centerXAnchor.constraint(equalTo: view.centerXAnchor) // add
])
}
@objc func openModal(_ sender: UIButton) {
let modalViewController = ModalViewController.init()
modalViewController.modalPresentationStyle = .fullScreen
present(modalViewController, animated: true, completion: nil)
}
// add
@objc func openNavigation(_ sender: UIButton) {
let firstPageViewController = FirstPageViewController.init()
let customNavigationViewController = UINavigationController.init(rootViewController: firstPageViewController)
present(customNavigationViewController, animated: true, completion: nil)
}
}
ボタンの追加等は今まで通りです。
openNavigation
メソッドを見てもらうとわかるように先程のpresent
を呼ぶ前にUINavigationControllerのイニシャライズをして、そのrootViewControllerに表示したいViewControllerを渡します。
SecondPageViewControllerに遷移する
FirstPageViewController
からSecondPageViewController
にナビゲーションを利用して遷移するためにFirstPageViewController
のnextPage
メソッドを編集します。
@objc func nextPage(_ sender: UIButton) {
let secondPageViewController = SecondPageViewController.init()
navigationController?.pushViewController(secondPageViewController, animated: true)
}
UINavigationViewControllerでラップした場合、viewControllerはnavigationControllerを持つため、pushViewController
メソッドを呼んで遷移させたいViewControllerのインスタンスを渡します。
まとめ
ざっくりとアプリケーションを組み立てる上で必要な部分を長々と書きましたが、自分のプロジェクトでは少しずつStoryboardを脱却し、今ではほぼコードで組み立てるようになっています。現在はRxSwift + MVVMを取り入れてアプリケーションを書くように頑張っているところです。
Discussion