✍️

Main.Storyboardを用いずに画面を表示する方法

2022/06/18に公開

書き尽くされた感あるネタですが、私も例にならって整理しておきます。用語の扱いが不適切な箇所がありましたらすみません、わかり次第修正していきます。

方法としてどのようにするかというだけでなく、なぜそのように書くのかや、なんのために行うのかをなるべく重視してまとめられたらと思います。ちなみに、画面の表示にはXIBを利用します。

画面表示の各種方法のメリット、デメリットに関しては、英語ですが下記のMediumが短くまとめられていてわかりやすいです。
https://manasaprema04.medium.com/xib-vs-storyboard-vs-creating-views-programatically-5c293bb825a

環境

名称 内容
ProductName: macOS
ProductVersion: 12.2.1
BuildVersion: 21D62
Terminal
% sw_vers
名称 内容
swift-driver version: 1.26.21
Apple Swift version: 5.5.2
Target: x86_64-apple-macosx12.0
Terminal
% swift -version

手順

手順は大きく3つの考えに分けられます。

1.Main.Storyboardに関わる記述の削除

Main.Storyboardを使わないのでその設定を削除します。その設定を削除しないと、Storyboardの設定は残っているのに、接続先のビューがないため、次のようなエラーが表示されます。

2. Storyboardに代わる画面の生成と保持

Main.storyboardを使わない以上、代わりに表示する画面が必要です。実際、Storyboardとその接続の設定を削除した上で起動すると次のような画面になります。これはUIWindowのみが表示されている状態です。

Storyboardとその設定は内部で表示するためのインスタンスの生成や保持を図ってくれるので、普段意識することはありません。そのStoryboardがなくなるので、代わりにコードで行う必要があります。

3. 画面の内容としてのビュー生成

上記の画面の生成と保持は、Storyboardで言うところの接続の設定です。接続先の具体的なビューである、Main.Storyboardに代わるものとして利用されるのがxibです。xibをプロジェクト上に用意して、シミュレーターで表示することまでできたら完成です。

*今後はSwiftUIのようにコードで作られていくかもしれませんが、ここではxibのみをビューとして利用します。

それぞれの手順を詳しくみていきます。

Main.Storyboardに関わる記述の削除

いまの環境での削除する箇所をキャプチャ画像で示します。

  • Main.Storyboard自体の削除

  • TARGETS より Depoloyment Info 上の Main Interface から "Main" の名称を削除

  • Info.plist より "Main stroyboard file base name" の項目の削除

    ちなみに、Info.plistはなんのためにその情報が載せられているのかという理解が難しい部分が少なくないのですが、プロジェクト作成時に含まれているのはほぼUISceneやUIWindowに関わりのある項目です。後述するAppDelegateとSceneDelegateの違いを把握しておかれると全くわからんという状態から少なからず関連性を見出せるようになってくるように思います。

Storyboardに代わる画面の生成と保持

SceneDelegate上に次のようなコードを記述します。必要に応じて適宜書き換えてください。

SceneDelegate.swift
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
	//①
        guard let scene = (scene as? UIWindowScene) else { return }
	
        //② MARK: - 記述はここから
        let window = UIWindow(windowScene: scene)
	//③
        self.window = window
        //④
	window.makeKeyAndVisible()
        //⑤
	let vc = ViewController()
        //⑥
	window.rootViewController = vc
	// - MARK: ここまで
    }
}

コードで行っていることを順番に見ていきます。

guard let scene = (scene as? UIWindowScene) else { return }
はじめから記述されているコードです。引数からUISceneを受け取ります。UISceneに関してはここでは触れませんが、UIKitから直接与えられるパラメータであり、直接呼ぶことはありません。

let window = UIWindow(windowScene: scene)
新たなUIWindowのインスタンスを作成します。引数windowSceneは①のUISceneを受け取ります。

self.window = window
プロパティに代入し、起動している間は保持します。保持できなければアプリになにも載せられなくなるからですね。self.windowはこのSceneDelegateクラスが起動している間は保持されているので、必然的にプロパティも保持されているということだと思います。

window.makeKeyAndVisible()
保持されたwindowに対してmakeKeyAndVisible()を呼び出し、アプリの階層に載せます。

let vc = ViewController()
windowに載せるビューをコントロールするためのUIViewControllerを生成します。このUIViewControllerは後述するxibに結びつけたクラスを指定します。

window.rootViewController = vc
windowのプロパティであるrootViewControllerに、生成したUIViewControllerを代入し保持させます。
self.windowと同じく、起動時からAppDelegateやSceneDelegateはクラスとして保持されるため、プロパティに代入したUIViewControllerのインスタンスも保持されます。

画面の内容としてのビューの用意

ビューの作り方としてはStoryboard、Xib、Programaticallyなものとに分けられますが、ここではXIB(ジブ)でビューを用意するとします。つまり、この記事においては、Main.Storyboardを削除するということはStoryboard → XIBへの切り替えを意味します。といっても、やることは単純です。

⌘ + Nでテンプレート一覧を表示してCocoa Touch Classを選択します。

その上で Also create XIB fileを選択します。これで終わりです。以前はOwnerなどの設定を見直す必要があったようですが、今は直接作って上記のようにコードを書けば反映されています。

実行

実行してみるときちんと表示されていることがわかります。この状態ではすでにMain.Storyboardの存在はありません。

そもそもSceneDelegateとは

SceneDelegateはiOS13から登場した比較的新しいクラスです。作られた目的の一つとして、iPadの登場によってWindowを分割して表示することが必要となったという点が挙げられます。たとえば、Info.plistにはEnableMultipleWindowという項目がありますが、これはマルチなWindow、複数の画面の設定項目です。

これまで見てきたように、SceneDelegateクラスはUISceneやUIWindowを扱います。その登場以前は上記のようなコードはAppDelegateクラスで全て扱われてきました。そのAppDelegateクラスの責務が分割され、アプリのライフサイクルが関わるイベントはAppDelegateに、Sceneに関わることはSceneDelegateとなりました。

登場以前はAppDelegateが担っていたことから、SceneDelegateに書いたことをAppDelegateに移したとしても、おそらく動くはずです。このような経緯から、Main.Storyboardを削除してXIBで表示する方法として、以前はAppDelegateに必要な記述をするといった記事が多く書かれていました。もし、SceneDelegateがあれば、結果的に不要となるので削除されます。

SceneDelegateを利用しているわけでもないので、そちらの方が不要なクラスを記述しないという点でプロジェクトをコンパクトにまとめられるとも思えるのですが、上記のような責任の区別を考えると、今後はSceneDelegateに書いておいた方が良さそうです。

ちなみに、学習し初めの頃はこのAppDelegateとSceneDelegateがなんのためにあるのか本当によくわからず、大変混乱していました。一年くらいかけてようやく少しだけ飲み込めるようになった次第です。

今回整理するにあたって調べていて、特にわかりやすい記事があったので紹介します。

https://medium.com/@kalyan.parise/understanding-scene-delegate-app-delegate-7503d48c5445
この記事は抜群に読みやすくて、そしてクラスの役割について理解しやすいです。なんのためのものなのかを学びたい人にとって本当に適切なレベルで解説してくれていると感じます。

https://manasaprema04.medium.com/scene-delegate-vs-appdelegate-86e22dc17fcb
こちらは冒頭でも紹介した方.こちらは曖昧にしてしまいがちな用語についても整理してくれているので、上記読んだ後であれば比較的ついていきやすいと思います。読むとInfo.plistなどに対するとっつきにくさも少しだけ解消されるのではないかと思います。ちなみに、この方はウォルマートのエンジニアさんみたいです。

最後に、Appleのドキュメントを紹介しておきます。
https://developer.apple.com/documentation/uikit/app_and_environment/scenes
(実はまだ読んでおらず)

Discussion