👻

Storyboardを使わないiOSアプリ開発

2020/12/02に公開

もう今は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以前のアプリケーションでは利用できません。

SceneDelegate.swift
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を利用します。

AppDelegate.swift
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にはまだコードを書いていない状態です。わかりやすく表示されているか確認するために背景を赤にします。

ViewController.swift
override func viewDidLoad() {
    super.viewDidLoad(
    view.backgroundColor = .red
}

Simulatorにビルドして背景が赤くなります。

「Hello world!」を表示してみる

テキストを表示するのでUILabelを使っていきます。
配置は基本的にはAutoLayoutを利用します。

まずはViewControllerにUILabelをストアドプロパティとして定義します。

ViewController.swift
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の相対的な関係を定義するものになります。

ViewController.swift
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)
    ])
}

上記はtitleLabelviewのX軸、Y軸の中心が同じになるということを表しています。
つまりはviewの中心に配置するという意味です。これを実行してみます。

中心に表示されました。Storyboardのときに比べてコードを書く量は増えますが、それなりにメリットはあるように思います。ただし、AutoLayoutが若干理解するのに難しいです。

  • topAnchor
  • bottomAnchor
  • leadingAnchor
  • trailingAnchor
  • centerYAnchor
  • centerXAnchor

これらを使って定義します。

AtomicDesign等で要素を分解して、パーツ、コンポーネントでUIViewを作っておくと、後はサクサク組み立てて埋め込んでいく、みたいな感じにできそうです。

画面遷移をする

Storyboardではボタンクリックをしたら画面遷移させる等もGUIでできていました。しかしコードオンリーになった今それを使うことができません。Storyboardにはidentifierを設定して、コード側でSegue遷移させる方法もありますが、Stringで管理しないと行けないはアプリケーションが大きくなるにつれ辛くなりそうなので使いたくはありませんでした。

  • 下から出てくる画面遷移
  • Navigationを使った画面遷移

まずは遷移させるために新しくViewControllerを作ります。クラス名は ModalViewControllerにしました。特に意味はありません。

背景の色をわかりやすくGreenにしておきます。それ以外は特に変えていません。

ModalViewController.swift
import UIKit

class ModalViewController: UIViewController {

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

下から出てくる画面遷移

さてViewController -> ModalViewControllerへと遷移させるため、ボタンを配置し、そのボタンがタップされたときに画面遷移するようにします。記述したコードは以下の様になっています。

ViewController.swift
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をセットすると全画面になります。

ViewController.swift
let modalViewController = ModalViewController.init()
modalViewController.modalPresentationStyle = .fullScreen
present(modalViewController, animated: true, completion: nil)

Navigationを使った今回の遷移は ViewController > FirstPageViewController > SecondPageViewControllerにします。

まずはFirstPageViewControllerを作ります。

FirstPageViewController.swift
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を作ります。

SecondPageViewController.swift
import UIKit

class SecondPageViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .orange
    }
}

SecondPageViewControllerはわかりやすくオレンジにします。

さて、次にFirstPageViewControllerに遷移するためにViewControllerの中身を変更していきます。

ViewController.swift
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にナビゲーションを利用して遷移するためにFirstPageViewControllernextPageメソッドを編集します。

FirstPageViewController.swift
@objc func nextPage(_ sender: UIButton) {
    let secondPageViewController = SecondPageViewController.init()
     navigationController?.pushViewController(secondPageViewController, animated: true)
}

UINavigationViewControllerでラップした場合、viewControllerはnavigationControllerを持つため、pushViewControllerメソッドを呼んで遷移させたいViewControllerのインスタンスを渡します。

まとめ

ざっくりとアプリケーションを組み立てる上で必要な部分を長々と書きましたが、自分のプロジェクトでは少しずつStoryboardを脱却し、今ではほぼコードで組み立てるようになっています。現在はRxSwift + MVVMを取り入れてアプリケーションを書くように頑張っているところです。

参考

Swift実践入門

Discussion