SwiftUIでアプリ開発をしていると、「画面が表示されたときに処理を実行したい」という場面によく遭遇します。そんなときに使うのが.onAppear
モディファイアです。
本記事では、SwiftUI初心者の方にもわかりやすく、.onAppear
の基本から実践的な使い方、注意点までを徹底的に解説します。
.onAppearとは何か
.onAppear
は、ビューが画面に表示される直前に実行されるモディファイアです。UIKitのviewDidAppear
に相当する機能で、SwiftUIにおける「画面表示時の処理」を実装する基本的な方法です。
基本的な構文
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.onAppear {
print("ビューが表示されました")
}
}
}
このコードでは、Text
ビューが画面に表示される直前に、コンソールに「ビューが表示されました」と出力されます。
.onAppearの実行タイミング
.onAppear
は以下のタイミングで実行されます。
- アプリ起動時、ビューが最初に表示されるとき
- ナビゲーションで画面に戻ってきたとき
- タブを切り替えて再び表示されたとき
- シートやアラートを閉じて元の画面に戻ったとき
.onAppearの実践的な使い方
1. データの読み込み
画面表示時にAPIからデータを取得する、最も一般的な使用例です。
struct UserListView: View {
@State private var users: [User] = []
@State private var isLoading = false
var body: some View {
Group {
if isLoading {
ProgressView("読み込み中...")
} else {
List(users) { user in
HStack {
Image(systemName: "person.circle")
Text(user.name)
}
}
}
}
.onAppear {
loadUsers()
}
}
func loadUsers() {
isLoading = true
// APIからユーザーデータを取得
Task {
do {
let fetchedUsers = try await APIClient.fetchUsers()
users = fetchedUsers
isLoading = false
} catch {
print("データ取得エラー: \(error)")
isLoading = false
}
}
}
}
// ユーザーモデル
struct User: Identifiable {
let id: Int
let name: String
}
2. 画面表示の分析・トラッキング
どの画面がどれくらい見られているかを分析するために使います。
struct ProductDetailView: View {
let product: Product
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
AsyncImage(url: URL(string: product.imageURL))
.frame(height: 300)
Text(product.name)
.font(.title)
.bold()
Text("¥\(product.price)")
.font(.title2)
.foregroundColor(.blue)
Text(product.description)
.font(.body)
}
.padding()
}
.onAppear {
trackScreenView()
}
}
func trackScreenView() {
// アナリティクスツールに送信
Analytics.track("商品詳細表示", properties: [
"product_id": product.id,
"product_name": product.name,
"product_price": product.price
])
print("商品詳細が表示されました: \(product.name)")
}
}
struct Product {
let id: Int
let name: String
let price: Int
let description: String
let imageURL: String
}
3. タイマーやアニメーションの開始
画面表示時にタイマーを開始し、画面を離れるときに停止する例です。
struct TimerView: View {
@State private var seconds = 0
@State private var timer: Timer?
@State private var isRunning = false
var body: some View {
VStack(spacing: 30) {
Text("経過時間")
.font(.headline)
Text("\(seconds)秒")
.font(.system(size: 60, weight: .bold))
.foregroundColor(.blue)
HStack(spacing: 20) {
Button(isRunning ? "一時停止" : "開始") {
if isRunning {
stopTimer()
} else {
startTimer()
}
}
.buttonStyle(.borderedProminent)
Button("リセット") {
resetTimer()
}
.buttonStyle(.bordered)
}
}
.padding()
.onAppear {
startTimer()
}
.onDisappear {
stopTimer()
}
}
func startTimer() {
isRunning = true
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
seconds += 1
}
}
func stopTimer() {
isRunning = false
timer?.invalidate()
timer = nil
}
func resetTimer() {
stopTimer()
seconds = 0
}
}
4. ユーザー設定の読み込み
画面表示時に保存された設定を読み込む例です。
struct SettingsView: View {
@AppStorage("isDarkMode") private var isDarkMode = false
@AppStorage("fontSize") private var fontSize = 16.0
@State private var userName = ""
var body: some View {
Form {
Section("表示設定") {
Toggle("ダークモード", isOn: $isDarkMode)
HStack {
Text("文字サイズ")
Slider(value: $fontSize, in: 12...24, step: 1)
Text("\(Int(fontSize))")
}
}
Section("ユーザー情報") {
TextField("ユーザー名", text: $userName)
}
}
.onAppear {
loadUserSettings()
}
}
func loadUserSettings() {
// UserDefaultsから設定を読み込む
if let savedUserName = UserDefaults.standard.string(forKey: "userName") {
userName = savedUserName
}
print("設定を読み込みました")
print("ダークモード: \(isDarkMode)")
print("文字サイズ: \(fontSize)")
}
}
5. キーボードの表示
特定のテキストフィールドに自動的にフォーカスする例です。
struct LoginView: View {
@State private var email = ""
@State private var password = ""
@FocusState private var isEmailFocused: Bool
var body: some View {
VStack(spacing: 20) {
Text("ログイン")
.font(.largeTitle)
.bold()
TextField("メールアドレス", text: $email)
.textFieldStyle(.roundedBorder)
.keyboardType(.emailAddress)
.focused($isEmailFocused)
SecureField("パスワード", text: $password)
.textFieldStyle(.roundedBorder)
Button("ログイン") {
// ログイン処理
}
.buttonStyle(.borderedProminent)
}
.padding()
.onAppear {
// 画面表示時に自動的にメールアドレス欄にフォーカス
isEmailFocused = true
}
}
}
.onAppearを使う際の重要な注意点
注意点1: 複数回呼ばれる可能性がある
.onAppear
は、ビューが表示されるたびに呼ばれます。これは予想外の動作を引き起こすことがあります。
struct ContentView: View {
@State private var count = 0
var body: some View {
NavigationStack {
VStack(spacing: 20) {
Text("表示回数: \(count)")
.font(.title)
NavigationLink("詳細画面へ") {
DetailView()
}
}
.onAppear {
count += 1
print("onAppear呼ばれました: \(count)回目")
}
}
}
}
struct DetailView: View {
var body: some View {
Text("詳細画面")
.navigationTitle("詳細")
}
}
このコードでは、詳細画面に移動して戻ってくるたびにcount
が増えます。これが問題になる場合は、次の解決策を使います。
注意点2: 初回のみ実行したい場合の対処法
画面が何度表示されても、処理を1回だけ実行したい場合があります。
方法1: フラグを使う
struct ContentView: View {
@State private var hasAppeared = false
@State private var data: [String] = []
var body: some View {
List(data, id: \.self) { item in
Text(item)
}
.onAppear {
guard !hasAppeared else { return }
hasAppeared = true
// 初回のみ実行したい処理
loadInitialData()
}
}
func loadInitialData() {
print("初回データ読み込み")
data = ["項目1", "項目2", "項目3"]
}
}
方法2: .task を使う(iOS 17以降推奨)
struct ContentView: View {
@State private var data: [String] = []
var body: some View {
List(data, id: \.self) { item in
Text(item)
}
.task {
// 自動的に初回のみ実行される
await loadInitialData()
}
}
func loadInitialData() async {
print("初回データ読み込み")
try? await Task.sleep(nanoseconds: 1_000_000_000)
data = ["項目1", "項目2", "項目3"]
}
}
注意点3: 非同期処理を正しく扱う
.onAppear
内で非同期処理を実行する場合は、Task
を使います。
struct NewsListView: View {
@State private var articles: [Article] = []
@State private var isLoading = false
@State private var errorMessage: String?
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView("読み込み中...")
} else if let errorMessage = errorMessage {
VStack {
Image(systemName: "exclamationmark.triangle")
.font(.largeTitle)
Text(errorMessage)
.foregroundColor(.red)
Button("再試行") {
loadArticles()
}
}
} else {
List(articles) { article in
VStack(alignment: .leading) {
Text(article.title)
.font(.headline)
Text(article.summary)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
}
.navigationTitle("ニュース")
.onAppear {
loadArticles()
}
}
}
func loadArticles() {
isLoading = true
errorMessage = nil
Task {
do {
// 非同期でデータを取得
let fetchedArticles = try await NewsAPI.fetchArticles()
articles = fetchedArticles
isLoading = false
} catch {
errorMessage = "データの取得に失敗しました"
isLoading = false
}
}
}
}
struct Article: Identifiable {
let id: Int
let title: String
let summary: String
}
// ダミーAPI
struct NewsAPI {
static func fetchArticles() async throws -> [Article] {
try await Task.sleep(nanoseconds: 2_000_000_000)
return [
Article(id: 1, title: "SwiftUI 最新情報", summary: "新機能が追加されました"),
Article(id: 2, title: "iOS 18 リリース", summary: "最新版がリリースされました")
]
}
}
注意点4: メモリリークに注意
タイマーやObserverを使う場合は、必ず.onDisappear
で解放します。
struct NotificationListenerView: View {
@State private var notificationCount = 0
var body: some View {
VStack {
Text("通知数: \(notificationCount)")
.font(.title)
}
.onAppear {
setupNotificationObserver()
}
.onDisappear {
removeNotificationObserver()
}
}
func setupNotificationObserver() {
NotificationCenter.default.addObserver(
forName: NSNotification.Name("CustomNotification"),
object: nil,
queue: .main
) { _ in
notificationCount += 1
}
}
func removeNotificationObserver() {
NotificationCenter.default.removeObserver(
self,
name: NSNotification.Name("CustomNotification"),
object: nil
)
}
}
.onAppear vs .task – どちらを使うべきか
iOS 17以降では、多くの場合.task
の方が適切です。
.onAppear を使うべき場合
- 同期的な処理を実行する
- 単純な初期化処理
- iOS 16以前もサポートする必要がある
.onAppear {
print("画面が表示されました")
setupInitialState()
}
.task を使うべき場合(iOS 17以降)
- 非同期処理を実行する
- ネットワークリクエストを行う
- ビューが消えたときに自動的にキャンセルしたい
.task {
await loadData()
}
比較表
項目 | .onAppear | .task |
---|---|---|
実行タイミング | ビュー表示時 | ビュー表示時 |
非同期処理 | Taskで囲む必要あり | 直接awaitが使える |
自動キャンセル | なし | ビュー非表示時に自動 |
iOS対応 | すべてのバージョン | iOS 17以降 |
メモリリーク対策 | 手動で必要 | 自動 |
実例比較
onAppearを使った場合
struct ContentView: View {
@State private var data: [String] = []
var body: some View {
List(data, id: \.self) { item in
Text(item)
}
.onAppear {
Task {
await loadData()
}
}
}
func loadData() async {
try? await Task.sleep(nanoseconds: 2_000_000_000)
data = ["データ1", "データ2", "データ3"]
}
}
taskを使った場合(推奨)
struct ContentView: View {
@State private var data: [String] = []
var body: some View {
List(data, id: \.self) { item in
Text(item)
}
.task {
await loadData()
}
}
func loadData() async {
try? await Task.sleep(nanoseconds: 2_000_000_000)
data = ["データ1", "データ2", "データ3"]
}
}
よくある質問と回答
Q1: .onAppearが呼ばれないのはなぜ?
A: 以下の可能性があります。
- ビューが実際には表示されていない(条件分岐で非表示になっている)
- 親ビューが再描画されていない
- NavigationStackやTabViewの構造に問題がある
// ❌ 条件分岐で非表示の場合は呼ばれない
if showView {
Text("表示")
.onAppear {
print("これは呼ばれる")
}
} else {
Text("非表示")
.onAppear {
print("これは呼ばれない")
}
}
Q2: .onAppearとinitの違いは?
A: 実行タイミングが異なります。
init
: ビューのインスタンスが作成されたとき(まだ画面に表示されていない).onAppear
: ビューが実際に画面に表示される直前
struct ContentView: View {
init() {
print("1. init実行")
}
var body: some View {
Text("Hello")
.onAppear {
print("2. onAppear実行")
}
}
}
Q3: リスト内の各要素で.onAppearを使える?
A: はい、可能です。スクロールして要素が表示されるたびに実行されます。
struct ContentView: View {
let items = Array(1...100)
var body: some View {
List(items, id: \.self) { item in
Text("項目 \(item)")
.onAppear {
print("項目 \(item) が表示されました")
// 無限スクロールの実装例
if item == items.last {
loadMoreItems()
}
}
}
}
func loadMoreItems() {
print("さらにデータを読み込む")
}
}
実践的なコード例集
例1: 検索画面での使用
struct SearchView: View {
@State private var searchText = ""
@State private var searchHistory: [String] = []
@FocusState private var isSearchFieldFocused: Bool
var body: some View {
NavigationStack {
VStack {
TextField("検索...", text: $searchText)
.textFieldStyle(.roundedBorder)
.focused($isSearchFieldFocused)
.padding()
List(searchHistory, id: \.self) { query in
HStack {
Image(systemName: "clock")
Text(query)
}
}
}
.navigationTitle("検索")
.onAppear {
loadSearchHistory()
isSearchFieldFocused = true
}
}
}
func loadSearchHistory() {
if let history = UserDefaults.standard.array(forKey: "searchHistory") as? [String] {
searchHistory = history
}
}
}
例2: プロフィール画面での使用
struct ProfileView: View {
@State private var userName = ""
@State private var userEmail = ""
@State private var profileImage: Image?
@State private var isLoading = true
var body: some View {
ScrollView {
VStack(spacing: 20) {
if isLoading {
ProgressView()
} else {
if let profileImage = profileImage {
profileImage
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(Circle())
} else {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 100, height: 100)
.foregroundColor(.gray)
}
Text(userName)
.font(.title)
.bold()
Text(userEmail)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding()
}
.onAppear {
loadUserProfile()
}
}
func loadUserProfile() {
Task {
isLoading = true
// ユーザー情報を読み込む
try? await Task.sleep(nanoseconds: 1_000_000_000)
userName = "山田太郎"
userEmail = "yamada@example.com"
isLoading = false
}
}
}
まとめ
.onAppear
は、SwiftUIで画面表示時の処理を実装する基本的なツールです。
重要なポイント
- 基本的な使い方 – ビュー表示時に実行される処理を定義
- よくある用途 – データ読み込み、分析、タイマー開始など
- 複数回実行される – タブ切り替えやナビゲーションで再度呼ばれる
- 初回のみ実行 – フラグを使うか、
.task
を使う - 非同期処理 –
Task
で囲むか、.task
を使う - メモリリーク対策 –
.onDisappear
でクリーンアップ - iOS 17以降 – 非同期処理には
.task
の使用を推奨
使い分けの目安
- 同期的な処理・単純な初期化 →
.onAppear
- 非同期処理・ネットワークリクエスト →
.task
(iOS 17以降) - タイマーやObserver →
.onAppear
+.onDisappear
のセット
.onAppear
を適切に使うことで、ユーザーエクスペリエンスの高いアプリを作ることができます。初心者の方は、まず基本的なデータ読み込みから始めて、徐々に複雑な処理に挑戦していきましょう。