Xcodeで新しいiOSアプリのプロジェクトを作成すると、勝手にStoryboardやらViewControllerやらが生成されて、
何もしなくても真っ白な画面のアプリができると思います。
これそもそもどういう構造になっているんだ? というのをずっと曖昧なままやってきたので、整理する意味でこの記事を書きます。
丁寧に書くと、本当に1冊の本になるくらいの内容なので、説明が足らない部分や英文直貼りになってしまう箇所はご容赦ください。
ドキュメントについて
昔は「iOSアプリケーション プログラミングガイド」なる日本語ドキュメントがあったっぽいのですが、
Swift化あたりで焚書されたみたいです。
検索したところ、アーカイブにも見つからないです。。。
About App Development with UIKit
多分この内容が、それに相当するのだと思われます。
基本的には公式ドキュメントに準拠して説明していきますが、それ以外が出典の場合は明記します。
iOSアプリのざっくりした構造
Xcodeの生成するサンプルアプリのアーキテクチャは、MVCモデルです。
アーキテクチャについては、ViewControllerが肥大化する問題があるため、
将来のスケールを見越すのであれば、MVVMやClean Architectureを導入するのが常識的になっています。
「ViewController.swift」がController部分に相当します。
Modelについてはサンプルには存在せず、ViewについてはStoryboardでいじくる想定になっています。
以上がざっくり説明です。
これから、もうちょっと詳しく見ていきましょう。
UIKitについて
iOSアプリで開発者がUIを実装するために、UIKitというフレームワークが提供されています。
UIKit provides many of your app’s core objects, including those that interact with the system, run the app’s main event loop, and display your content onscreen. You use most of these objects as-is or with only minor modifications. Knowing which objects to modify, and when to modify them, is crucial to implementing your app.
UI部品を生成するために、なんとなくUIKit使うと思いますが、
それだけではなく、iOSアプリのメインクラスやスクリーンの抽象クラスなどもUIKitが提供しています。
iOSアプリのStatus
Managing Your App's Life Cycle
iOSアプリは5つのState(状態)を持ちます。
(※iOS13以降のUISceneDelegateを使ったアプリは別)
- Active
- Inactive
- Background
- Not Running
- Suspended
それぞれの状態は、UIApplicationのapplicationStateというプロパティで参照可能。
ただし、Not Running / Suspendedの2状態は、アプリが処理できない状態なので、アプリ内からは取得できません。
(自分が生まれる前や死んでる状態で、「あれ俺生きてるのかな?」って聞けないですよね?)
Active
アプリケーションが起動して、ユーザーが操作可能な状態。
要するにアプリ起動状態。
Inactive
Activeへと状態遷移する前、あるいはActiveから状態遷移する前に、一瞬だけ入る状態。
Background
ユーザーは操作不可能だが、iOS上では何らかの処理をしている状態。
バックグラウンド処理中。
いくつか語りたいことがあるので、詳しくは後述。
Not Running
起動していない状態。
iPhoneのイメージで言えば、アプリの切替をしようとしても存在しない状態。
Suspended
メモリ不足などで、iOSからアプリが停止中にされている状態。
Backgroundについて
iOS4.0の登場以前(端末で言うとiPhone4よりも前)では、なんとiOSはマルチタスクができませんでした。
[iOS][小ネタ] アプリのバックグラウンド実行を禁止する方法
したがってBackgroundというStateも存在せず、起動してるor起動中or未起動というミニマリズムな設計だったようです。
このBackgroundという状態ですが、開発者が自由に何でもできるわけではなく、
アプリとして必要最低限のことをやれ、というお触れが出ています。
When your app is in the background, it should do as little as possible, and preferably nothing. If your app was previously in the foreground, use the background transition to stop tasks and release any shared resources. If your app enters the background to process an important event, process the event and exit as quickly as possible.
Preparing Your UI to Run in the Background
翻訳はしんどいのでしませんが、要は「Backgroundの処理は最低限にしとけ」ということです。
正確にBackground状態が何分の処理まで行ける、という情報は、僕が探した限りだと公開してないみたいです。
何かで5分間と見た気がするが、僕の勘違いですか?
StackOverflowによると最長で3分らしいです。
もしコードが書きたければ、下記が参考になると思います。
Appleが想定しているのは、下記の処理みたいです。
- Release Resources upon Entering the Background(資源の解放)
- Prepare Your UI for the App Snapshot(アプリ切替画面のスナップショットの準備)
- Respond to Important Events in the Background(重要イベントの処理)
このうち大事なのは、重要イベントの処理ってやつで、
たとえばユーザーの位置情報をトラッキングしたい(Google MapのTimelineみたいな)などは、
定期的にバッググラウンド処理を走らせたいですね。
Apps don’t normally receive any extra execution time after they enter the background. However, UIKit does grant execution time to apps that support any of the following time-sensitive capabilities:
- Audio communication using AirPlay, or Picture in Picture video.
- Location-sensitive services for users.
- Voice over IP.
- Communication with an external accessory.
- Communication with Bluetooth LE accessories, or conversion of the device into a Bluetooth LE accessory.
- Regular updates from a server.
- Support for Apple Push Notification service (APNs).
この辺の処理は、Background状態になったアプリでも、
定期的に資源をiOSが割り振って、一瞬だけ処理をさせてくれるようになっています。
ちなみにこの仕組みを悪用して、Facebookが昔無音ファイルの再生をしていて、問題になったらしいです。
Facebookアプリがバッテリーを無駄に消費する原因だと発覚、バックグラウンド動作をオフにしても無音ファイル再生で無効化
状態遷移図
状態の遷移図ですが、上記のようになっています。
通常の起動処理であれば、
Not Running → Inactive → Active
または、
Not Running → Background( → Inactive → Active)
という遷移をします。
Inactiveに行くか、Backgroundに行くかは、
depending on whether the UI is about to appear onscreen.
とのこと。
要は開発者の設定次第ですね。
起動した瞬間に運悪くメモリ不足が発生しているようなケースだと、
Not Running → Suspended
という経路もありえます。
Activeなアプリが、ユーザー操作で別アプリに切り替えられたり、途中で電話アプリに割り込まれたりした場合、
Active → Inactive → Background
という遷移をします。
ユーザーがアプリ切替画面で明示的にアプリを終わらせる、または端末の電源を落とすなどした場合、
Background → Not Running
と遷移します。
iOSがアプリを起動してからViewを生成するまでの流れ
状態の話が済んだところで、iOSがアプリを起動してからViewを生成する流れを見ていきたいと思います。
なぜそんなことに興味を持ったかというと、なんとなく「self.view」とか書いたり、「UIScreen.shared」とか書いたりしていて、
背景を知らなかったので、なぜこの書き方でインスタンスとれてるのか、またどこのインスタンスとっているのかが曖昧だったためです。
iOSアプリに起動経路
iOSアプリの起動は、ユーザーが端末でそのアプリアイコンをタップする、というのが基本です。
ただ現在のiOS13まで来たiOSアプリは、起動経路も多様になっています。
- アイコンタップ
- Notificationからの起動
- ウィジェットからの起動
- Universal Links/DeepLinkによる起動
などなど……
これらの起動経路のハンドルも考えないといけないのですが、その方法も用意されているので、後述します。
UIApplicationとは
アプリケーションはOSから起動され、コンピュータ資源を割り振られます。
iOSでも同様です。
アプリの起動要求がかかると、iOSはUIApplicationのインスタンスを生成します。
UIApplicationはiOSアプリそのものを表現するクラスです。
シングルトンになっており、開発者は「UIApplication.shared」と記述することで、iOSが生成したインスタンスを参照できます。
このUIApplicationの中身は、アプリの個別の処理を書く場所ではないので、開発者はサブクラスを作って何か書く、ということは通常ないはずです。
したがって、基本的には1アプリ = 1UIApplicationです。
……とずっと思っていたのですが、ドキュメント見ると、
Every iOS app has exactly one instance of UIApplication (or, very rarely, a subclass of UIApplication)
と書いてあり、仕様としてUIApplicationのサブクラス作成が禁止されているわけではないみたいです。
まあ1インスタンスかはともかくとして、必ず1つ以上存在します。
実は前述の全体図にも登場済みです。
UIApplicationが文字通りiOSアプリの骨組みと言えるでしょう。
Event Loopとは?(main関数はどれだ?)
全体図を見ると、Event Loopというのがあります。
iOSアプリに限らず、ユーザーインターフェイスのあるアプリケーションは、
初期処理を終えた後にユーザーからの応答待ちになり、ユーザーから見ると止まっているように見えますが、内部的にはループ処理でユーザーの操作を待つ状態になります。
これがEvent Loopです。
他のフレームワークでプログラミング経験があって、iOS開発にやってきた方は疑問に思うと思うのですが、
(特にC言語やJava)
iOS開発をしていると、main関数が出てきません。
じゃあmain関数は存在しないのか、というと、そんなことはありません。
隠蔽されているだけです。
そしてEvent Loopもそのmain関数の中でやっています。
Xcodeが自動生成する「appDelegate.swift」の中に、答えがあります。
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
この「@UIApplicationMain」というAttributeが、メイン関数の役割を果たしています。
UIApplicationMain(::::)を呼んでいるみたいです。
UIApplicationMain
Apply this attribute to a class to indicate that it’s the application delegate. Using this attribute is equivalent to calling the UIApplicationMain function and passing this class’s name as the name of the delegate class.
If you don’t use this attribute, supply a main.swift file with code at the top level that calls the UIApplicationMain(::::) function. For example, if your app uses a custom subclass of UIApplication as its principal class, call the UIApplicationMain(::::) function instead of using this attribute.
ちなみに英文に書いてあるとおり、@UIApplicationMainを使わないで、自前でUIApplicationMain(::::)を呼んでアプリの起動処理を書くことも可能です。
(メリットはないですが)
詳しくは「親切すぎるiPhoneアプリ開発の本」とかを読むといいと思います。
AppDelegateとは
話がちょっと前後しますが、AppDelegate(Application Delegate)ってなんでしょう?
Xcodeが勝手に生成するコードの中の1つなので、初心者だとよくわかないまま、とりあえずネット上でググって、
よくわからないままここにコードを追加した経験があるのではないでしょうか?
実は、UIApplicationのDelgateメソッドこそ、AppDelegateです。
ユーザーは自分でUIApplicationの実装をしませんが、
UIApplicationが生成されたタイミングですぐ実行したい処理や、バックグランドに行くときに実行したい処理は書きたいですよね?
UIApplicationには、前述の状態遷移に応じて、Delegateメソッドが用意されています。
AppDelegateの詳細は公式ドキュメント見てくれ以外にないんですが、@KenNagami
さんがわかりやすい図を作成してくれているので、下記を見るといいと思います。
なおiOS13以降からSceneDelegateという概念が爆誕しましたが、まだ僕がキャッチアップできていないため、詳細は触れません。
アプリの起動経路
長くなりすぎて書いていて息切れしてきましたが、もうちょっと書きたいことあるので書きます。
アプリの起動経路は、下記で取得できます。
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
return true
}
UIApplication.LaunchOptionsKeyで詳細は見てください。
Viewの生成について
UIApplicationのお仕事は、Event Loopの生成ともう1つ、Viewの生成という大事な仕事があります。
A major role of your app’s application object is to handle the initial routing of incoming user events. It dispatches action messages forwarded to it by control objects (instances of the UIControl class) to appropriate target objects. The application object maintains a list of open windows (UIWindow objects) and through those can retrieve any of the app’s UIView objects.
https://developer.apple.com/documentation/uikit/uiapplication
UIWindowのインスタンスを生成して、AppDelegateのwindowプロパティに参照を持ちます。
UIWindowの生成はどう設定したらいいのでしょう?
(なおこれ以降の記述は「親切すぎるiPhoneアプリ開発の本」を参考にしています)
Interface Builderとの紐付け
プロジェクトファイルに、実はその設定があります。
このMain Interfaceに指定されたパラメータに従って、UIWindowが生成されます。
初期設定ではmainになっており、「main.Storyboard」の内容を指しています。
プロジェクトファイルやStoryboardファイルは、Xcodeで見るとGUIで見られるので、
実体がわかりづらくなりますが、実際はこれらも1ファイルです。
UIWindow
UIWindowを使うケースは下記です。
You use windows only when you need to do the following:
- Provide a main window to display your app’s content.
- Create additional windows (as needed) to display additional content.
1アプリに対して、最低1windowの構成です。
UIApplicationは禁止こそされていないものの、ほぼ1インスタンスでしたが、
UIWindowはメインWindow以外を生成するケースがまあまああります。
たとえばソフトウェアキーボードも、1つのWindow扱いです。
ちなみにwindowLevelという概念で、UIの上下関係を制御しています。
UIWindowのwindowLevelについて あるいはソフトウェアキーボードより上にViewを表示する方法
さて、UIWindowはrootViewControllerというプロパティを持ちます。
これが皆さんおなじみのViewControllerの参照先です。
ViewControllerの生成
いよいよここまで来ましたね。
初期設定では、「main.Storyboard」に1つのViewControllerと1つのViewが用意されています。
この指示に従って、UIApplicationは、UIWindowのrootViewControllerにViewControllerを割り当てます。
ViewControllerはUIViewのインスタンスへの参照をviewというプロパティで持っています。
故に、
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(self.view.frame) //Viewのframeを表示。selfは省略可
}
みたいな書き方で、viewにアクセスできるわけです。
以上の初期設定故に、最初にシミュレータを起動すると、真っ白な画面のiOSアプリが起動する(=UIApplicationとmain.Storyboard上のUIViewの結びつけが完了している)のです。
ViewControllerのライフサイクルとかも軽くまとめようかと思いましたが、
思いのほか大作になってしまいましたし、そのへんの記事はたくさんあると思うので、他の記事を探してみてください。
参考と感想
iOS アプリの構造がどのようになっているか紐解いてみる
素晴らしい記事ですが、内容がObjective-C時代でちょっと古いのが辛いですかね。
「親切すぎるiPhoneアプリ開発の本」
この記事を書くにあたって、参考にさせていただいた良本です。
「iOSアプリ開発の体系的な入門書がない問題」という記事を書いたんですが、この本はかなり求めていたものに近かったです。
初心者→中級者のレベルという感じ。
1冊目にとる本としては高度すぎますが、ちょっとアプリ開発かじって、曖昧なまま使っていた概念をちゃんと説明してくれた本です。
この本しか説明してない内容も結構あるんじゃないですかね?