はじめに
こんにちは。初心者です
これまで、Swiftのクラスや、クラスを利用したDelegateのことを勉強してきました
ですが、Swiftの文法を勉強している初心者のほとんどの人が書籍を読んだり、記事を読んだりして勉強しているとき、こう思ったことではないでしょうか
「で、これどこで使うんだ??」
Swiftを勉強しているということは、99.9%の人がiOSアプリの開発が目標あるいは目的だと思います
なんやかんやして文法を勉強していて、なんとなく分かったなーと思っても、それが実際にどう使われているのか分からないとモチベーションは上がらないのは当然だと思います。Swiftという言語とアプリがどのように関連しているのか想像がしづらいと思います
それだと、本質的に理解をすることは難しいなあと痛感したので、今回の記事では『アプリとSwiftってこんな関係なんだ〜』、『アプリってこんな仕組みで動いているんだな〜』ということを知ってもらえれば幸いです
今回もこれまでの記事は読了前提で書いていくので、よければ是非読んで行ってください(ダイマ)
↓
https://qiita.com/Noka0323/items/684d0f4cf719a7c1bcc6
https://qiita.com/Noka0323/items/9246425c0da8dc0d720d#delegate
そもそもiOSアプリとは何なのか
Swiftを勉強している私達はiOSアプリを作りたい訳ですが、iOSアプリを作るにはiOSアプリが何なのかについて理解している必要があります
iOSアプリがどのように動いているのか
どのiPhoneにも入っているAppStoreを実際に見ながら、iOSアプリがどのようなものなのか、改めて見ていきましょう
AppStoreのアイコンを押すと、アプリが起動します
アプリが起動すると、Todayの画面が表示されます
TodayはAppStoreのアプリが起動した時に、最初に必ず表示される画面です
一番上にあるアプリの紹介をタップしてみましょう
紹介の部分をタップすると、アプリの詳細な紹介が画面に表示されました
画面をスクロールすると、iPhoneの画面のサイズには収まりきらなかった紹介を見ることが出来ます
画面の右上にある×印を押すと、アプリの紹介画面が閉じます
そして、Todayの画面が再び表示されます
表示されているアプリをタップすると、そのアプリの詳細が表示されます
この一連で、iOSアプリがどういうものなのかが少し見えてきたのではないでしょうか
iOSアプリは人間(ユーザー)の操作によって、表示する画面が変わり、その表示された画面を見て、人間(ユーザー)が再び操作をするという無限ループが構成されています
つまり、iOSアプリは起動、画面の表示、操作の処理の三つのプロセスによって成り立っているということです
そして、画面の表示と操作の処理はループしており、このループのことをイベントループといいます
iOSアプリは何でできているのか?
では、早速タイトル回収してきましょう
iOSアプリが一体何でできているのか
それはコードです
あとはいくつかの画像や必要があればアニメーションと音声etc……が少々
アプリを起動するのはコードです
画面をスクロールした時、表示しているアプリの画面を動かしているのもコードです
画像を表示するのもコードです
アプリ内で検索する際にうごしているのもコードです
サーバーと通信するのもコードです
ぜ〜〜〜〜〜〜〜んぶコードです
では、コードは一体何でできているのでしょうか
答えはSwiftというプログラミング言語で書かれたコードです
Objective-Cも正解です
あんまり書く必要もないとは思いますが、一応ここでのコードの定義はコンピューターへの指示をプログラミング言語によって書かれたテキストの塊という意味です
つまりiOSアプはSwift(もしくはObjective-C)で書かれたコードの巨大な塊ということです
ですが、ここでもう一つの疑問が生まれてきます
なんでネイティブで開発する際に使用するプログラミング言語がSwift(もしくはObjective-C)に限定されるのか?という話です
どうしてiOSはAndroidのようにJavaが使えないのでしょう?
でも、FlutterでもiOSは開発できます
JavaとFlutterには何の違いがあるのでしょう?
その答えはフレームワークにあります
※ここから先はObjective-Cのことは割愛します
先ほど、アプリを起動するのも、スクロールした画面を動かすのも、画像を表示するのも、検索するのも全てコードで動かしていると言いました
では、毎回アプリを作るたびに、アプリを起動するコードを書いたりするのかというとそうではありません
理論上は書くことは不可能ではありませんが、多分誰も書いたことないと思います
何故なら、既にAppleがアプリを起動するコードを用意してくれているからです
アプリを起動するコードだけではなく、アプリを終了するコード、先述した画像を表示するコードも用意しています
その数、なんと数百個以上あるらしいです。すごいですね
なので、基本的にあらかじめAppleによって用意されたコードを組み合わせて、iOSアプリは作られています
このコードのまとまりのことをフレームワークと言い、iOSではそれをUIKitもしくはSwiftUIと呼びます
UIKItもしくはSwiftUIはSwiftで書かれているので、当然iOSアプリの開発ではプログラミング言語をSwiftに限定されるという訳です
ちなみに、今回はUIKitを用いて話をしていきます
そして、Flutterもフレームワークの名前です
FlutterはDartという言語を用いて書かれており、開発ではDartを用います
ところで、アプリを起動するコードとあっさり言いましたが、おそらくちょ〜〜〜〜複雑と思います。Appleの社員ではないので見たことはありませんが
そんな超複雑コードをベタベタ貼り付けたら何が起こるでしょうか
そう、みんな大好きバグです
それだけではありません。このアプリではこう起動したい、このアプリではああ起動したいとカスタマイズしたい場合もあります(というかほぼ毎回そう)
バグを発生させず、毎回のカスタマイズに対応する。そんな夢みたいなコードの書き方があるわけ……
あるんです!!!
そう、オブジェクト指向ならね!!!!!
オブジェクト指向が何なのかはこの記事でちょろっと書きました
↓
https://qiita.com/Noka0323/items/684d0f4cf719a7c1bcc6
一度クラスを作ってしまえば、何度もインスタンス化させて使いたい放題
継承、Delegateし放題。カスタマイズし放題
超複雑なコードでもバグを怖がらずに使い回しができるという訳です
なので、UIKitではアプリで使われる機能は主にクラスで書かれています
それとは対照的にSwiftUIでは構造体(Struct)が用いられています
アプリはどうやって起動するのか
ところで、クラスの記事の際にコードを実行する為に、コマンドラインツールを使っていました
コマンドラインツールとは、グラフィカルな要素(UI)のないアプリを作成するプロジェクトの理解で問題ありません
コマンドラインツールでは、main.swiftに書かれたコードを実行し終わると、同時にアプリも強制的に終了します
ですが、それに対してiOSアプリは人間がアプリを終了するという操作をしない限り、アプリが終了することはありません(ただし、エラーによるクラッシュは除く)
このiOSアプリとコマンドラインツールの違いは一体なんなのでしょう?
そして、どうしてアプリはループしているのでしょう?
まず無限ループを開始するにはアプリを起動する必要があります
アプリを起動すると言うと、なんだか特別なことのように感じますが、アプリを起動するということはプログラムを実行するということとほぼ同義です
起動をすることで、処理が実行されます
そして、本来プログラムはどこから実行するかを定義しなければ、実行することが出来ません
その実行を始める地点のことを、エントリーポイントと呼びます
何故、エントリーポイントが必要かと言うと、だいたいの場合アプリケーションは複数のファイルによって構成されているからです。ファイルAの先頭から実行して欲しいとコンピュータに指示を予め出しておかないとと、コンピュータはどのファイルから実行していいのか分からなくなります
C言語やObjective-C言語では、このエントリーポイントの役割をmain関数と呼ばれる関数がこなし、人間が独自に定義しなければいけません
ところで、UIKitでプロジェクトを作成すると、必ずこの7つのファイルが入っています
こちらはコマンドラインツールのプロジェクトです
どちらのファイルにも、立ち上げた時点ではプログラムの実行の起点(エントリーポイント)となるはずのmain関数がありません。
ですが、画面左上のRunボタンを押すと、普通にプロジェクトは実行されます
エントリーポイントであるmain関数がないにも関わらずです
では、コンピュータはどこをエントリーポイントだと認識しているのでしょう?
コマンドラインツールにおいては、Xcodeはmain.swiftをエントリーポイントとして認識しています
Xcodeはmain.swiftを特別扱いしており、設定を変えない限りmain.swiftから必ず実行されるということが、同時にSwiftという言語における独自の特徴でもあります
では、iOSアプリではどうなのでしょう?
答えは、AppDelegate.swiftから起動しています
どうしてAppDelegateから起動していることが分かるのかというと、AppDelegate.swiftに@mainが記載されているからです
この@mainが一体何なのかはファイルの中に具体的に記述されてはいませんが、何だかコマンドラインツールのmain.swiftと関係がありそうな名前をしていますね
では、@mainが一体何なのかを調べていきましょう
この@mainが何者なのかは、ここに書いてあります
↓
https://developer.apple.com/documentation/uikit/uiapplicationdelegate/3656306-main
ざっくりと読むと、@mainが書かれたクラスは@main属性(アトリビュート)を付与され、@main属性が付与されたクラスはUIAplicationクラスにエントリーポイントとしてみなされるみたいなことが書いてあります
そして、UIAplicationMain関数によって、そのエントリーポイントからプロジェクトが起動されます
もう一度AppDelegateを見てみると、がっつり@mainと書かれていますね
なので、UIKitで作られたアプリは基本的にAppDelegateから起動することになります
ところで、先ほどはあっさりと流しましたが、@mainの解説で出てきたUIApplicationとは一体何なのでしょうか?
@mainの説明を読んでいると、Delegateがあったり関数があったりするようです
では、@mainのページにリンクがあったUIAplicationDelegateの公式ドキュメントによる説明も見てみましょう
https://developer.apple.com/documentation/uikit/uiapplicationdelegate
英語はさっぱり分かりませんが、どうやらUIApplicationDelegateはクラスではなく、プロトコルのようです
そして、UIApplicationDelegateはUIApplicationクラスと連携しており、アプリのライフサイクルを管理する為に利用されているらしいです
アプリのライフサイクルとは、アプリの起動、起動したアプリの維持、バックグランドでのアプリの維持、アプリの終了などの一連の動作のことです
では、今度はUIApplicationDelegateと連携していると書かれている、UIApplicationクラスの説明を見てきます
https://developer.apple.com/documentation/uikit/uiapplication
UIApplicationはUIResponderクラスを継承したクラスのようです
そして、アプリが起動すると、UIApplicationMain関数によってインスタンスが作成されます
その際、作成されるUIApplicationクラスのインスタンスは一つのみで、シングルトンパターンの例に漏れずshardeプロパティを利用することでメソッドにアクセスすることが出来ます
UIApplicationの役割は、iOSとiOSアプリの連携で、基本的に人間がプロジェクトファイルの中でUIApplicationのコードを書き換えるということはありません
情報を一度整理しましょう
まず、iOSアプリを起動する為には人間がアプリのアイコンをタップする必要があります。そして、アイコンをタップする操作はiPhoneに搭載されているiOSが検知します
iOSはアプリアイコンがタップされたことをUIApplicationクラスに伝えます
UIApplicationクラスはアプリを起動する為に、UIApplicationMain関数によってインスタンスを作成します
そして、作成されたUIApplicationインスタンスがiOSアプリを起動します
これでiOSアプリが起動する理屈は分かりました
ですが、まだUIApplicationDelegateが何の為に存在しているかは分かっていませんし、ただアプリが起動するだけではなく、ログイン機能を付けるなどの起動時に独自の処理を行うことを求められる場合もあります
起動時に独自の処理をする為にはどうするべきか
これまでの説明で、UIKitでiOSアプリがどういったプロセスで起動するかは分かりました
主にiOSとUIAplicationクラスが起動を行ってくれるので、人間が何か特別にコードを書く必要はありません
ですが、全てのアプリがこのようなシンプルな起動をしている訳ではありません
私達が普段使っているiOSアプリは起動の際に、独自の様々な処理を行っています
たとえば、初めて使うアプリでは初回ログインが求められたりします
また、iPhoneに入っている天気アプリは位置情報を取得することで、現在地の天気を表示してくれます
こういった、アプリが起動する際に行われる独自の処理を行うにはどうすればいいのでしょう?
アプリを起動するクラスはUIApplicationクラスです。ですが、このクラスを直接書き変えたりすることはできません
なので、私達はUIAplicationクラス以外で、アプリが独自な起動をする為のコードを書かなければいけないのです
Delegateを学んだ時のことを思い出しましょう
https://qiita.com/Noka0323/items/9246425c0da8dc0d720d
私達はDelegateを学ぶ際、ランダムな数字を出力するクラスと数字を2倍するクラスを例に用いました
Delegateを利用して実現したのは、ランダムな数字を出力するクラスから出力された数字を、数字を2倍にするクラスを用いて2倍するコードです
UIApplicationクラスの立ち位置はランダムな数字を出力するクラスと同じです
現在の私達に必要なものは、数字を2倍するクラスと同じような立ち位置にいるクラスです
つまり、アプリがどのような起動するかを定めたクラスです
そして、そのクラスがAppDelegateクラスとSceneDelegateクラスです
前回の例に即してコードを書いてみると、何となくイメージがしやすいのではないでしょうか
protocol UIApplicationDelegate {
func application()
}
// 委譲する側
// 起動する側
class UIApplication {
var delegate: AppDelegate?
func launchApplication() {
// いろいろな処理
delegate?.application()
}
}
// 委譲される側
// どうやって起動するか定義する側
class AppDelegate: UIResponder, UIApplicationDelegate {
func application() {
// 実際の処理
}
}
AppDelegateクラスとSecneDelegateクラスの二つがありますが、基本的な役割は変わりません
どちらも、アプリがどのような起動をするのかを定めたクラスです
では、何故似たようなクラスが二つもあるのかというと、SceneDelegateはiOS13以降のマルチウィンドウ(複数のアプリの画面を同時に表示させること)に対応する為に、後から追加されました
基本的にマルチウィンドウをしたい場面はiPadでアプリを使う時くらいな気がするので、作るアプリがiPhoneのみでの使用を想定している場合は、AppDelegateでいいのではないかと思います
iOSアプリを作る場合は好きな方でいいです
逆に言えば、iPadのアプリとして展開いく予定がある場合はSecneDelegateにしておいた方が丸いです
まず、AppDelegate.swiftを見ると、AppDegateというクラスがどういうクラスなのかある程度分かります
まず、UIResponderというクラスを継承しており、UIApplicationDelegateというプロトコルを採用しているということです。複数のクラスを継承することは出来ないので、コロンのあとのものがクラスだった場合は必ず継承になります
そして、メソッドがずらずらと並んでいる訳ですが、一番上のメソッドを見てみましょう
コメントには「アプリを起動後をカスタマイズする為のオーバーライドポイント」的なことが書いてあります
要するに、アプリが起動する時に特別なことがしたい時は、このメソッドの中に処理を書くということです
他にもアプリ終了時に実行されるメソッドや、バックグラウンド状態の時に実行されるメソッドもあります
AppDelegateとSceneDelegateはいつ、誰がインスタンス化しているのか
ところで、以前のクラスの記事で、「クラスはインスタンス化しないと機能を利用できない」ということを書きました
↓
https://qiita.com/Noka0323/items/684d0f4cf719a7c1bcc6
作成したばかりのプロジェクトを改めて見てみましょう
初めからあるのはAppDelegate.swiftとSceneDelegate.swiftとViewController.swiftとMainStorybordとLaunchScreen.storyboardです
コードが書かれているのはAppDelegate.swiftとSceneDelegate.swiftとViewController.swiftの三つで、そこにAppDelegateクラスとSceneDelegateクラスとViewControllerクラスが書かれています
では、この三つをインスタンス化しているコードはどこにあるのでしょうか?
これです。定数名は適当につけてます
↓
let appDelegate = AppDelegate()
let secneDelgate = ScnedeDelegate()
let viewController = ViewController()
プロジェクトの中にはどこにも存在していません
なのにも関わらず、ビルドすると普通にアプリが起動します
ということは、どこかしらでクラスがインスタンス化されているから、アプリのビルドが成功したということになっているはずです
では、プロジェクト内でAppDelegateクラスとSecneDelegateクラスとViewControllerクラスの三つをインスタンス化するコードが無いにも関わらず、アプリのビルドが成功する理由とは一体何なのでしょう
答えは、先ほどからチラチラと出てきていたUIApplicationMain関数にあります
UIApplicationMain関数はアプリを起動する関数で、起動の際にUIApplicationクラスをインスタンス化します
そして、UIApplicationMain関数がインスタンス化するクラスはUIApplicationクラスだけではなく、AppDelegateクラスとSecneDelegateクラス、そしてMainStorybordに紐づいているViewControllerもインスタンス化します
https://developer.apple.com/documentation/uikit/1622933-uiapplicationmain
つまり、プロジェクト内にこれを書かなくても、AppDelegateクラスとSceneDelegateクラスとViewControlerクラスは必ずUIApplicationMain関数によってインスタンス化されるということです
なので、プロジェクトを立ち上げたばかりの初期の状態でもアプリがビルドする訳です
これ
↓
let appDelegate = AppDelegate()
let secneDelgate = ScnedeDelegate()
let viewController = ViewController()
逆に言うと、後から自分で追加したクラスは必ずプロジェクトのどこかで、インスタンス化するコードを自分で書かなければいけません
画面とは何なのか
アプリを起動させることは出来ました
では、アプリを起動した際に、次に何が起こるのが画面の表示です
これまでの説明でiOSアプリがAppleの用意したクラスの集合体であることは分かっているので、画面もまた何らかのクラスの機能によって実現したもののか、何らかのクラスのインスタンスであることは想像できると思います
では、いつも皆さんが見ているアプリの画面が何で構成されているかと言うと、答えは主にUIWindow、UIView、ViewControllerの三つです
UIWindow
まず、UIWindowクラスではウィンドウを作ります
ウィンドウとはiPhoneの画面の中で、どれだけの広さでアプリの画面を表示するかを決めている領域のことです。基本的にウィンドウの大きさとiPhoneの画面の大きさは同じになることが大半です
これはiOS特有の概念ではなくて、わりとよく聞くブラウザウィンドウのウィンドウのことです
↓
https://wa3.i-3-i.info/word1542.html
表示しているものがスマホアプリかブラウザかの違いだけです
ですが、先程のSecneDelegateの説明で上げたように、iOS13以降はマルチウィンドウという機能が追加されているので、必ずしもウィンドウの大きさとiPhoneの画面の大きさがイコールになるという訳ではありません
UIView
そして、UIViewはテキストや画像といったビジュアル面のコンテンツを担当します
たとえば、ストーリーボードにlabelを配置します
そして、それをoutletで繋ぐとこうなります
UILabel!型の変数labelが自動で追加されました
では、UILabel型がいったい何なのかを調べてみましょう
https://developer.apple.com/documentation/uikit/uilabel
UILabelクラスとは一行以上のテキストを表示する為のビューのようです
そして、UIViewから継承されています
また、画像を表示する為のUIImageViewもUIViewを継承しています
ViewConroller
最後のViewConrollerは、現在どの画面を表示するかを決めるのが主な役割のクラスです
現状のiOSアプリは複数のViewを持っているのが当たり前となっており、ユーザーからの操作によってそれを逐次切り替えています
この切り替えるという役割をこなしているのが、ViewControllerです
ちなみに、画面が切り替わることを画面遷移と呼びます
これは余談ですが、基本的に画面が切り替わる際に行うアクションはボタンをタップする、あるいは画面をスワイプするかのどちらかです
buttonのクラスはUIbuttonであり、UIControlというクラスから継承しています。そして、UIControlの継承元はUIViewとなっています
ですが、buttonを押された際、どのような処理を行うかを定めるのはUIResponderクラスを継承しているViewControllerクラスです
実際にアプリを作ってみる
ここまでつらつらと説明してきた訳ですが、結局説明してるだけではどう活用していくのかさっぱり分からないので、これまで説明してきたものを全部使って実際にアプリを作ってみました
今回作ったのは、1番目の画面にあるTextFieldに「login」と入力すると2番目の画面に遷移するアプリです
間違ったパスワードを入れるとアラートが出ます
遷移先の画面には、1番目の画面で入力した「login」が表示されます
そして、予め「login」が入力されていると、初めから2番目の画面が表示されます
イメージ的には初回ログイン時にパスワードの入力が求められて、2回目以降は自動でログインされてるアプリという感じです
メインストーリーボードは削除してあります
何故かと言うと、Xcodeはデフォルトでメインストーリーボードを一番最初に表示する設定があるからです
ですが、今回はその設定があると不都合なので、デフォルトでメインストーリーボードを表示する設定を削除し、ついでにメインストーリーボードも削除しています
どうして削除する必要があるかは、後々説明して行きます
まず、改めて今回作ったアプリに必要な要件を一つ一つまとめていきましょう
- 最初にログイン画面を表示する
- すでにパスワードが入力されている場合は、最初から2番目の画面を表示するようにする
- ログイン画面でパスワードを入力する
- 入力されたら正誤を判定する
- パスワードが合っていたら、2番目の画面に遷移する
- パスワードが間違っていたら、アラートを出す
- 2番目の画面に、1番目の画面で入力したパスワードを表示する
これを全部コードで実現していきます
1.最初にログイン画面を表示する
ログイン画面とはこの画面のことです
最初にログイン画面を表示するということは、言い換えると、「初めにログイン画面を表示するように起動する」 ということでもあります
つまり、起動時に独自な処理を加えたいということです
こんな時にどのようなクラスを使うかは、すでにこの記事の中で紹介しました
AppDelegateクラス、もしくはSceneDelegateクラスです
ですが、ここで疑問に思った人もいるのではないでしょうか
どうして、メインストーリーボードでは駄目なのか?という話です
メインストーリーボードをログイン画面にすれば、Xcodeが自動的に最初にメインストーリーボードを表示させてくれるので、AppDelegateやSceneDelegateにコードを書かずに済みます
なのに、わざわざ新しい画面とクラスを用意し、コードを書かなければ理由とは一体なんなのでしょうか?
2. すでにパスワードが入力されている場合は、最初から2番目の画面を表示するようにする
必要な要件その2とはすでにパスワードが入力されている場合は、最初から2番目の画面を表示するようにするです
今回は様々な都合上、実際にパスワードがすでに入力されている状態にする訳ではないのですが、ここで重要なのは最初に表示する画面が条件によって変わるという点です
ログイン画面を表示して欲しい場合もあれば、2番目の画面が表示されていて欲しい場合もあります
なので、常にメインストーリーボードが最初に必ず表示される設定というのは非常に邪魔なのです
だから、メインストーリーボードとその設定を消す必要があります
この二つの設定です
↓
メインストーリーボードを消したので、表示したい画面は自分で追加する必要があります
今回、自分で追加した画面はFirstViewControllerとSecondViewControllerの二つです
FirstViewController
SecondViewController
命名の由来は1番目に表示する画面だからFirstViewController、2番目に表示するのだからSecondViewControllerです
FirstViewControllerの方には色々とコードが書いてありますが、中身は後々に説明します
起動時に独自の処理を加える場合は、AppDelegateかSceneDelegateを使う訳ですが、今回はSceneDelegateを使用します
理由は特にありません。強いて言えば、記事を書く際に参考にしていたものがSceneDelegateを使っていたからくらいのノリです
追加したコードの一覧です
SceneDelegateの一番上にあるメソッドが起動時の処理に関するものなので、今回は一番上にあるメソッドにコードを書いていきます
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
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).
guard let scene = (scene as? UIWindowScene) else { return }
//UIWindowクラスのインスタンスを作る
//表示する画面の大きさが指定される
//今回は特に何も指定してないのでiPhoneと同じ大きさになる
let window = UIWindow(windowScene: scene)
//FirstViewControllerとSecondViewControllerのストーリーボードをインスタンス化させる
let firstStoryboard = UIStoryboard(name: "FirstViewController", bundle: nil)
let secondStoryboard = UIStoryboard(name: "SecondViewController", bundle: nil)
var vc: UIViewController
// FirstViewControllerをデフォルトで表示
if let firstVC = firstStoryboard.instantiateViewController(withIdentifier: "FirstViewController") as? FirstViewController {
vc = UINavigationController(rootViewController: firstVC)
} else {
vc = UIViewController()
}
//SecondViewControllerを表示する条件を追加
if let secondVC = secondStoryboard.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController {
secondVC.loadViewIfNeeded()
secondVC.label.text = "ダミー"
if secondVC.label.text == "login" {
vc = secondVC
}
}
//変数vcはSecondViewController型かFirstViewController型なので、rootViewControllerに設定できる
//rootViewcontrollerは一番最初に表示する画面のこと
window.rootViewController = vc
//定数windowの参照カウントを増やしておいてインスタンスがメモリから解放されないようにしておく
//これがないと画面が表示されない
self.window = window
//どのwindowを表示させるか指定する
//iOSアプリのwindowは基本一つ
window.makeKeyAndVisible()
}
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
@IBOutlet weak var textField: UITextField!
@IBAction func goToScondVC(_ sender: UIButton) {
//入力されたパスワードがあっている場合の処理
//ボタンを押したらSecondViewControllerに遷移
if textField.text == "login",
let vc = UIStoryboard(name: "SecondViewController", bundle: nil).instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController {
//SecondViewControllerにある変数loginIDとtextFieldを一緒だと定義しておく
//これで遷移先にtextFieldの中身が渡されて表示される
vc.loginID = textField.text
self.navigationController?.pushViewController(vc, animated: true)
} else {
//入力されたパスワードが間違っている場合の処理
//アラートを出す
let alert = UIAlertController(title: "パスワードが違います", message: "正しいパスワードを入れてください", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
}
class SecondViewController: UIViewController {
@IBOutlet weak var label: UILabel!
var loginID:String?
override func viewDidLoad() {
super.viewDidLoad()
//この画面が表示された時、一番最初に表示しておきたい内容を書いておく
label.text = loginID
}
}
SceneDelegate
コードによって起動時に表示する画面を指定する際の書き方は、割と定型的というか、大体同じ流れになると思います
今回はストーリーボードを使っているので型のキャストが所々入っていますが、ストーリーボードを使わない場合はもっとスマートになる……はずです
流れとしては
1.UIWindowクラスのインスタンスを作り、アプリを表示する領域の大きさを決めておく
2. 表示したい画面のViewControllerをインスタンス化しておく
3. インスタンス化の際に条件があれば、定めておく
4. rootViewController(一番最初に表示する画面のこと)を決める
5. UIWindowクラスのインスタンスが解放されないように、参照カウントを増やしておく
6. どのWindowを表示するを決める(※複数のウィンドウがある場合の対処。基本的にスマホであればウィンドウは一つ)
だいたいこんな感じになります
ストーリーボードを使う場合は、UIStorybordクラスのインスタンスをUIViewControllerを継承している型にキャストするコードが必要です
1. UIWindowクラスのインスタンスを作り、アプリを表示する領域の大きさを決めておく
//UIWindowクラスのインスタンスを作る
//表示する画面の大きさが指定される
//今回は特に何も指定してないのでiPhoneと同じ大きさになる
let window = UIWindow(windowScene: scene)
このコードです
大体の場合で、定数名はwindowになってます
ついでですが、このguard文の定数名はデフォルトだと省略されているのですが、これも大抵の場合でscneになっているので、変えておいた方が丸いです
guard let scene = (scene as? UIWindowScene) else { return }
2. 表示したい画面のViewControllerをインスタンス化しておく
//FirstViewControllerとSecondViewControllerのストーリーボードをインスタンス化させる
let firstStoryboard = UIStoryboard(name: "FirstViewController", bundle: nil)
let secondStoryboard = UIStoryboard(name: "SecondViewController", bundle: nil)
このコードです
ViewControllerをインスタンス化しておくと書いてあるのですが、今回はストーリーボードを使って画面を構築しているので、ストーリーボードの方をインスタンス化しておきます
ここのStorybordID(ストーリーボードのファイル名) を入力際に名前を間違えると、画面が表示されないので、間違えないないように気をつけましょう
3. UIViewController型のプロパティを用意しておく
var vc: UIViewController
このvcというプロパティに、後々インスタンス化した画面を代入します
なので、まだ値は代入しません
今回は変数になってますが、定数で定義できる場合もあります
今回変数になっている理由は、IF文が二つあるからです。多分
4. インスタンス化の際に条件があれば、定めておく
// FirstViewControllerをデフォルトで表示
if let firstVC = firstStoryboard.instantiateViewController(withIdentifier: "FirstViewController") as? FirstViewController {
vc = UINavigationController(rootViewController: firstVC)
} else {
vc = UIViewController()
}
//SecondViewControllerを表示する条件を追加
if let secondVC = secondStoryboard.instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController {
secondVC.loadViewIfNeeded()
secondVC.label.text = "ダミー"
if secondVC.label.text == "login" {
vc = secondVC
}
}
今回はFirstViewControllerのインスタンス化と、SecondViewcontrollerのインスタンス化を別々に行なっています
同じスコープの中で同時にインスタンス化の条件判定をできなくもないのですが、IF文の中にIF文がたくさんあるコードになるので、それはちょっとどうなのかと思ってやめました
そのあたりの是非はいったいどうなのでしょう。気になります
ちなみに、ストーリーボードを使わないとキャストがないので、こんな感じにスッキリしたコードになります
コードで起動させる際は、ストーリーボードを使わない方がいいかもしれません
let vc: UIViewController
let secondVC = SecondViewController()
secondVC.loadViewIfNeeded()
secondVC.label.text = "ダミー"
if secondVC.label.text == "login" {
vc = secondVC
} else {
let firstVC = FirstViewController()
vc = UINavigationController(rootViewController: firstVC)
}
4. rootViewController(一番最初に表示する画面のこと)を決める
//変数vcはSecondViewController型かFirstViewController型なので、rootViewControllerに設定できる
//rootViewcontrollerは一番最初に表示する画面のこと
window.rootViewController = vc
ここでUIViewController型のプロパティであるvcを用意してきたことが生きてきます
この時点で、vcに代入されているのは、FirstVieControllerのインスタンスかSecondViewControllerのインスタンスのどちらかです
なので、同じUIViewController型であるrootViewControllerにvcが代入できる訳です
rootViewControllerの説明です
https://developer.apple.com/documentation/uikit/uiwindow/1621581-rootviewcontroller
5.UIWindowクラスのインスタンスが解放されないように、参照カウントを増やしておく
//定数windowの参照カウントを増やしておいてインスタンスがメモリから解放されないようにしておく
//これがないと画面が表示されない
self.window = window
クラスとメモリについてはこの記事で解説しました
↓
https://qiita.com/Noka0323/items/684d0f4cf719a7c1bcc6
どうしてこのコードがないと画面が表示されないかというと、windowという定数の影響範囲がSceneDelegateというクラスの中の、sceneメソッドの中のスコープの中でしかないからです
なので、通常ならばSceneDelegateがUIApplocationMain関数によってインスタンス化されると、sceneメソッドの中でしか存在できない定数windowは、SceneDelagateがメモリから解放されると共にメモリから解放されます
ですが、今回はそれだと困るんですね
何故かというと、この定数windowがずっと残っていてくれないと、アプリを表示する領域の大きさを指定するクラスのインスタンスがなくなってしまうので、アプリが画面に表示されなくなるからです
なので、SceneDelagateのインスタンスがメモリから解放されても、定数windowがメモリから解放されないように、参照カウントを増やしておく必要があるのです
6.どのWindowを表示するを決める(※複数のウィンドウがある場合の対処。基本的にスマホであればウィンドウは一つ)
//どのwindowを表示させるか指定する
//iOSアプリのwindowは基本一つ
window.makeKeyAndVisible()
これに関しては、特に解説することはありません
コメントに書いてあるままです
ですが、これがないと画面が表示されないので忘れずに書いておきましょう
FirstViewController
ログイン画面に該当する画面です
FirsrtViewcontrollerという名前のストーリーボードと、FirstViewControllerという名前のクラスを紐づけています
class FirstViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
@IBOutlet weak var textField: UITextField!
@IBAction func goToScondVC(_ sender: UIButton) {
//入力されたパスワードがあっている場合の処理
//ボタンを押したらSecondViewControllerに遷移
if textField.text == "login",
let vc = UIStoryboard(name: "SecondViewController", bundle: nil).instantiateViewController(withIdentifier: "SecondViewController") as? SecondViewController {
//SecondViewControllerにある変数loginIDとtextFieldを一緒だと定義しておく
//これで遷移先にtextFieldの中身が渡されて表示される
vc.loginID = textField.text
self.navigationController?.pushViewController(vc, animated: true)
} else {
//入力されたパスワードが間違っている場合の処理
//アラートを出す
let alert = UIAlertController(title: "パスワードが違います", message: "正しいパスワードを入れてください", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
}
bouttonのメソッドに画面遷移の処理を書いてます
textFieldに入力された文字列が”login”であれば、SecondViewControllerへと遷移
そうでなければ、アラートを出すという処理です
画面遷移のフローとしてはこんな感じです
ボタンを押したら、遷移したい画面のインスタンスが作られる
↓
navigationControllerで遷移させる
↓
pushViewControllerでどういうふうに遷移させるかを決める
SecondViewController
パスワードを入力した後に表示される画面です
一番最初に表示される場合もあります
class SecondViewController: UIViewController {
@IBOutlet weak var label: UILabel!
var loginID:String?
override func viewDidLoad() {
super.viewDidLoad()
//この画面が表示された時、一番最初に表示しておきたい内容を書いておく
label.text = loginID
}
}
この変数loginIDはFirstViewControllerでtextField.textに代入してあります
これのおかげで、textFieldに入力した値をlabelで表示することができるようになります
最後に
もの凄く長々とした記事となりましたが、少しでもiOSアプリがどういうものなのかだとか、Swiftを何のために勉強するのかということへの理解になれば幸いです
今回サンプルとして作ったアプリも、ストーリーボードを使わなければよかったなあと色んなタイミングでやまほど後悔したので、気が向いたら後日そこだけ別記事にしてみようかと思います
(正直、力尽きた感は否めな……)
実際にアプリを作ってみると、クラスのインスタンス化→インスタンス化したものをどう使うかという流れの繰り返しだと感じたので、UIKitを使う際はクラスを重点的に勉強してみると、iOSアプリがどういうコードでできているのか理解しやすくなると思います