1. はじめに:ライフサイクル?——「お店の一日」で考える
要約
- 起動(開店)
- 表示(接客)
- 一時停止(小休止)
- バックグラウンド(裏で事務)
- 終了(閉店)
Xcode テンプレートの“入口”の違い
- UIKit:
AppDelegate
(店のオーナー)+SceneDelegate
(各店舗フロア) - SwiftUI App:
@main struct MyApp: App
(宣言的な入口)+Scene
(窓口)
2. UIKit 編:アプリ全体と画面のサイクル
2.1 アプリ/シーンの状態
iOS 13+ はシーン(マルチウィンドウ)前提。実務では「アプリ全体」よりUIScene
の遷移を観測することが多い。
- Not Running(未起動)
- Inactive(操作不可だが前面)
- Active(通常の前面動作)
- Background(裏で動作)
- Suspended(完全停止直前・再開は速い)
2.2 AppDelegate
と SceneDelegate
- 起動時の一度きりの初期化や SDK 構成は
AppDelegate
。 - 前面/背景などの遷移は
SceneDelegate
が担当(1ウィンドウ=1シーン)。
// AppDelegate.swift
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// SDK 初期化・ログ設定など(開店準備)
return true
}
// シーンを使う設定
func application(_ application: UIApplication,
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
}
// SceneDelegate.swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func sceneWillEnterForeground(_ scene: UIScene) { /* シャッター開ける直前 */ }
func sceneDidBecomeActive(_ scene: UIScene) { /* 接客開始 */ }
func sceneWillResignActive(_ scene: UIScene) { /* 一時休止・電話着信など */ }
func sceneDidEnterBackground(_ scene: UIScene) {
// 事務作業:軽い保存やタスク終了
UserDefaults.standard.synchronize()
}
}
2.3 画面単位:UIViewController
ライフサイクル
viewDidLoad
:1度だけ。ビュー生成・軽い初期化viewWillAppear
:表示直前。毎回viewDidAppear
:表示完了。アニメ開始・計測開始viewWillDisappear
/viewDidDisappear
:退場前後。タイマー停止・監視解除
OK/NG 例
OK:
- ✅ ネット読み込み開始:
viewDidAppear
(表示をブロックしない)
NG:
- ❌ 重い計算を
viewDidLoad
で同期実行(初回表示が固まる)
3. SwiftUI 編:宣言的UIのサイクルを掴む
3.1 @main
と App
/ Scene
SwiftUI はアプリの入口も宣言的。App
の body
で Scene
を返す。
@main
struct CounterApp: App {
@StateObject private var store = CounterStore()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(store)
}
}
}
3.2 View のフック:.onAppear
/ .onDisappear
/ .task
.onAppear
:見え始めた瞬間(何度も呼ばれる可能性).task
:表示時に非同期タスクを開始。id:
を付けると依存の変更で再実行.onDisappear
:見えなくなる瞬間。キャンセルやクリーンアップ
struct ContentView: View {
@State private var items: [String] = []
var body: some View {
List(items, id: \.self, rowContent: Text.init)
.task { items = await API.fetchItems() } // 表示時に1回
.onDisappear { /* タイマー停止・監視解除など */ }
}
}
3.3 アプリ状態の監視:scenePhase
お店の開店 / 休憩 / 閉店を受け取る感覚。
struct RootScene: View {
@Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var store: CounterStore
var body: some View {
ContentView()
.onChange(of: scenePhase) { old, new in
if new == .background { store.save() } // 裏に回ったら保存
if new == .active { store.refreshIfNeeded() }
}
}
}
3.4 データの寿命を意識する
@State
:View 内の短命データ@StateObject
:View が作る長生きな参照型 (初期化は1回)@ObservedObject
:外から注入された参照の監視@EnvironmentObject
:アプリ全体に配る共有依存- 便利:
@AppStorage
(アプリ全体の簡易永続)、@SceneStorage
(シーン単位)
3.5 タスクとキャンセルの勘所
- 重複実行を避けるなら
task(id:)
でトリガーを管理。 .task
は View が消えると 自動キャンセル。
struct AvatarView: View {
@State private var userID = "me"
@State private var image: Image?
var body: some View {
VStack {
image ?? Image(systemName: "person.circle")
}
.task(id: userID) { image = await API.avatar(for: userID) } // userID 変更時だけ再実行
}
}
4. UIKit と SwiftUI の対比&落とし穴
4.1 「どこで何をやる?」対応表(最小指針)
やりたいこと | UIKit の置き場 | SwiftUI の置き場 |
---|---|---|
SDK 初期化 | AppDelegate.didFinishLaunching | App.init |
前面になったら更新 | sceneDidBecomeActive | scenePhase == .active の .onChange |
背景へ行くと保存 | sceneDidEnterBackground | scenePhase == .background の .onChange |
画面が見えたらフェッチ | viewDidAppear | .task or .onAppear |
タイマー停止 | viewWillDisappear | .onDisappear |
4.2 よくある落とし穴
- 重い処理を同期で置く:
viewDidLoad
/body
に巨大計算→描画止まる (非同期に分離、スピナーはProgressView
などで。) - 非同期の多重起動:
onAppear
が再実行されがち (task(id:)
でトリガーを絞る orActor
/AsyncSequence
で直列化。) - 後始末忘れ:Timer / Notification / Combine の購読解除 (→ UIKit は
deinit
orviewWillDisappear
、SwiftUI は.onDisappear
/onChange
でキャンセル。)
4.3 併用パターン(ブリッジ)
UIKit → SwiftUI:UIHostingController(rootView:)
let vc = UIHostingController(rootView: ContentView())
window.rootViewController = vc
SwiftUI → UIKit:UIViewControllerRepresentable
struct MailVC: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MFMailComposeViewController { .init() }
func updateUIViewController(_ uiViewController: MFMailComposeViewController, context: Context) {}
}
5. 実践:ミニアプリで往復練習(同じ仕様で UIKit / SwiftUI)
仕様
- 画面にカウンター(+ / -)
- バックグラウンドに入ったら保存、復帰時に復元
- 保存先は
UserDefaults
(簡易)
5.1 UIKit
// モデル
final class CounterStore {
private let key = "count"
var count: Int = UserDefaults.standard.integer(forKey: "count")
// 保存
func save() {
UserDefaults.standard.set(count, forKey: key)
}
}
// SceneDelegate で保存フック
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
let store = CounterStore()
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let vc = CounterViewController(store: store)
window = UIWindow(windowScene: scene as! UIWindowScene)
window?.rootViewController = vc
window?.makeKeyAndVisible()
}
func sceneDidEnterBackground(_ scene: UIScene) {
// アプリがバックグラウンドに入るときに保存
store.save()
}
}
// 画面
final class CounterViewController: UIViewController {
private let store: CounterStore
init(store: CounterStore) {
self.store = store; super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError()
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
// + / - ボタンとラベルを配置(Auto Layout 省略)
}
@objc private func increment() {
store.count += 1 /* ラベル更新 */
}
@objc private func decrement() {
store.count -= 1 /* ラベル更新 */
}
}
5.2 SwiftUI 版(scenePhase
連動)
final class CounterStore: ObservableObject {
@Published var count: Int = UserDefaults.standard.integer(forKey: "count")
private let key = "count"
func save() {
UserDefaults.standard.set(count, forKey: key)
}
func refreshIfNeeded() {
/* 必要ならフェッチ等 */
}
}
@main
struct CounterApp: App {
@StateObject private var store = CounterStore()
@Environment(\.scenePhase) private var scenePhase
var body: some Scene {
WindowGroup {
CounterView().environmentObject(store)
}
.onChange(of: scenePhase) { _, new in
if new == .background { store.save() }
}
}
}
struct CounterView: View {
@EnvironmentObject private var store: CounterStore
var body: some View {
VStack(spacing: 16) {
Text("\(store.count)").font(.system(size: 48, weight: .bold))
HStack {
Button("-") { store.count -= 1 }.buttonStyle(.bordered)
Button("+") { store.count += 1 }.buttonStyle(.borderedProminent)
}
}
.padding()
}
}
// さらに簡単にするなら @AppStorage("count") var count = 0 を使えば保存処理すら不要。
5.3 チェックリスト
- 初期化は App(Delegate) に寄せる
- 表示直後のフェッチは
viewDidAppear
/.task
- 背景遷移で保存は
sceneDidEnterBackground
/scenePhase
- 監視解除・タイマー停止は 退場フック で
- 重い処理は非同期化、UI スレッドを塞がない
参考リンク
- Apple Docs: App and Environment(SwiftUI
App
/Scene
/scenePhase
)
https://developer.apple.com/documentation/swiftui/app - Apple Docs: View Life Cycle(
UIViewController
)
https://developer.apple.com/documentation/uikit/uiviewcontroller - Apple Docs: UIScene と ライフサイクル
https://developer.apple.com/documentation/uikit/uiscene - Apple Docs: UIApplicationDelegate
https://developer.apple.com/documentation/uikit/uiapplicationdelegate - Apple Docs: Task(Swift Concurrency)
https://developer.apple.com/documentation/swift/task - Apple Sample: Hosting SwiftUI in UIKit / Representable
https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit