MENU

【SwiftUI】.onAppearの使い方完全ガイド – 初心者向けに徹底解説

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で画面表示時の処理を実装する基本的なツールです。

重要なポイント

  1. 基本的な使い方 – ビュー表示時に実行される処理を定義
  2. よくある用途 – データ読み込み、分析、タイマー開始など
  3. 複数回実行される – タブ切り替えやナビゲーションで再度呼ばれる
  4. 初回のみ実行 – フラグを使うか、.taskを使う
  5. 非同期処理Taskで囲むか、.taskを使う
  6. メモリリーク対策.onDisappearでクリーンアップ
  7. iOS 17以降 – 非同期処理には.taskの使用を推奨

使い分けの目安

  • 同期的な処理・単純な初期化.onAppear
  • 非同期処理・ネットワークリクエスト.task(iOS 17以降)
  • タイマーやObserver.onAppear + .onDisappearのセット

.onAppearを適切に使うことで、ユーザーエクスペリエンスの高いアプリを作ることができます。初心者の方は、まず基本的なデータ読み込みから始めて、徐々に複雑な処理に挑戦していきましょう。

参考リンク

プログラミングの独学におすすめ
プログラミング言語の人気オンラインコース
独学でプログラミングを学習している方で、エラーなどが発生して効率よく勉強ができないと悩む方は多いはず。Udemyは、プロの講師が動画で実際のプログラムを動かしながら教えてくれるオンライン講座です。講座の価格は、セール期間中には専門書籍を1冊買うよりも安く済むことが多いです。新しく学びたいプログラミング言語がある方は、ぜひUdemyでオンライン講座を探してみてください。
目次