🔄

SwiftUIで画面描画時に一度だけ処理を実行するカスタムモディファイア

に公開

SwiftUIで画面描画時に一度だけ処理を実行するカスタムモディファイア

はじめに

SwiftUIで開発をしていると、「画面が表示されたときに一度だけ処理を実行したい」というケースがよくあります。例えば:

  • アプリ起動時のみデータを読み込む
  • チュートリアルを一度だけ表示する
  • 初回アクセス時のみAPIリクエストを送信する

SwiftUIの標準モディファイアである.onAppearは、ビューが表示されるたびに実行されます。これは画面が再描画されるたびに何度も実行される可能性があります。

この記事では、ビューが最初に表示されたときに一度だけ処理を実行するonAppearOnceモディファイアと、非同期処理用のtaskOnceモディファイアを実装してみましょう。

実装方法

OnAppearOnceModifier(同期処理版)

まずは同期処理用のモディファイアを実装します。

import SwiftUI

/// 画面描画時に一度だけ処理を実行するモディファイア
struct OnAppearOnceModifier: ViewModifier {
    // 処理を実行するアクション
    let action: () -> Void
    
    // 実行済みフラグを保持するための状態変数
    @State private var hasAppeared = false
    
    func body(content: Content) -> some View {
        content
            .onAppear {
                // まだ実行されていなければアクションを実行
                if !hasAppeared {
                    action()
                    hasAppeared = true
                }
            }
    }
}

/// 利便性のためにViewに拡張を追加
extension View {
    /// 画面描画時に一度だけ処理を実行するモディファイア
    /// - Parameter perform: 実行する処理
    /// - Returns: モディファイアが適用されたビュー
    func onAppearOnce(perform action: @escaping () -> Void) -> some View {
        modifier(OnAppearOnceModifier(action: action))
    }
}

このモディファイアの仕組みは以下の通りです:

  1. @Stateを使ってhasAppearedという実行済みフラグを保持します
  2. 通常の.onAppear内でフラグをチェックし、まだ実行されていなければ処理を実行します
  3. 処理実行後にフラグをtrueに設定することで、再描画時には実行されないようにします

TaskOnceModifier(非同期処理版)

次に、非同期処理用のモディファイアも実装しましょう。これはSwiftUIの.taskモディファイアをベースにしています。

/// Task実行版のモディファイア
struct TaskOnceModifier: ViewModifier {
    // 処理を実行するアクション
    let action: @Sendable () async -> Void
    
    // 実行済みフラグを保持するための状態変数
    @State private var hasExecuted = false
    
    func body(content: Content) -> some View {
        content
            .task {
                // まだ実行されていなければアクションを実行
                if !hasExecuted {
                    await action()
                    hasExecuted = true
                }
            }
    }
}

/// 利便性のためにViewに拡張を追加
extension View {
    /// 画面描画時に一度だけ非同期処理を実行するモディファイア
    /// - Parameter perform: 実行する非同期処理
    /// - Returns: モディファイアが適用されたビュー
    func taskOnce(perform action: @escaping @Sendable () async -> Void) -> some View {
        modifier(TaskOnceModifier(action: action))
    }
}

この非同期版も基本的な考え方は同じですが、@Sendable属性を使用して非同期処理に対応しています。

使用例

これらのモディファイアの使い方を示すデモビューを作成しましょう:

import SwiftUI

struct OnAppearOnceDemoView: View {
    @State private var message = "初期状態"
    @State private var count = 0
    @State private var asyncMessage = "初期状態"
    @State private var asyncCount = 0
    
    var body: some View {
        VStack(spacing: 20) {
            Text("OnAppearOnce モディファイアのデモ")
                .font(.title)
                .padding()
            
            Divider()
            
            // onAppearOnce デモセクション
            VStack(alignment: .leading, spacing: 10) {
                Text("onAppearOnce デモ")
                    .font(.headline)
                
                Text("メッセージ: \(message)")
                Text("カウント: \(count)")
                
                Button("画面を再描画") {
                    // 画面を強制的に再描画するための状態変更
                    count += 1
                }
            }
            .padding()
            .background(Color.blue.opacity(0.1))
            .cornerRadius(10)
            // ここでonAppearOnceを使用
            .onAppearOnce {
                message = "onAppearOnce が一回だけ実行されました"
                print("onAppearOnce アクションが実行されました")
            }
            
            Divider()
            
            // taskOnce デモセクション
            VStack(alignment: .leading, spacing: 10) {
                Text("taskOnce デモ")
                    .font(.headline)
                
                Text("非同期メッセージ: \(asyncMessage)")
                Text("非同期カウント: \(asyncCount)")
                
                Button("画面を再描画") {
                    // 画面を強制的に再描画するための状態変更
                    asyncCount += 1
                }
            }
            .padding()
            .background(Color.green.opacity(0.1))
            .cornerRadius(10)
            // ここでtaskOnceを使用
            .taskOnce {
                // 非同期処理を模擬
                try? await Task.sleep(nanoseconds: 1_000_000_000) // 1秒待機
                asyncMessage = "taskOnce が一回だけ実行されました"
                print("taskOnce アクションが実行されました")
            }
            
            Divider()
            
            // 比較のために通常のonAppearを使用
            VStack(alignment: .leading, spacing: 10) {
                Text("通常のonAppear(比較用)")
                    .font(.headline)
                
                Text("このセクションが再描画される度にonAppearが実行されます")
                Text("再描画カウント: \(count)")
            }
            .padding()
            .background(Color.red.opacity(0.1))
            .cornerRadius(10)
            .onAppear {
                print("通常のonAppearが実行されました(カウント: \(count))")
            }
        }
        .padding()
    }
}

このデモ画面では:

  1. onAppearOnceを使用した同期処理のセクション
  2. taskOnceを使用した非同期処理のセクション
  3. 比較用に通常のonAppearを使用したセクション

の3つを表示しています。それぞれのセクションに「再描画」ボタンがあり、これを押すとカウントが増加して画面が再描画されます。

通常のonAppearは再描画のたびに何度も実行されますが、onAppearOncetaskOnceは最初の1回だけ実行されることがわかります。

実際の使用シーン

これらのモディファイアは以下のようなシーンで特に役立ちます:

1. データの初期ロード

struct ContentView: View {
    @StateObject private var viewModel = ViewModel()
    
    var body: some View {
        List(viewModel.items) { item in
            Text(item.name)
        }
        .onAppearOnce {
            // 初回表示時のみデータをロード
            viewModel.loadInitialData()
        }
    }
}

2. 初回アクセス時のみのアニメーション

struct TutorialView: View {
    @State private var showHint = false
    
    var body: some View {
        VStack {
            Text("使い方のヒント")
                .opacity(showHint ? 1 : 0)
                .animation(.easeIn, value: showHint)
            
            // 他のコンテンツ
        }
        .onAppearOnce {
            // 初回表示時のみヒントをアニメーションで表示
            showHint = true
        }
    }
}

3. 非同期APIリクエスト

struct ProfileView: View {
    @StateObject private var viewModel = ProfileViewModel()
    
    var body: some View {
        VStack {
            Text("ユーザー: \(viewModel.user?.name ?? "Loading...")")
            // 他のプロフィール情報
        }
        .taskOnce {
            // 初回表示時のみAPIからプロフィールを取得
            await viewModel.fetchUserProfile()
        }
    }
}

まとめ

SwiftUIの標準モディファイアでは提供されていない「一度だけ実行する」という機能を、カスタムモディファイアとして実装しました。

  • onAppearOnce: 同期処理を一度だけ実行
  • taskOnce: 非同期処理を一度だけ実行

これらのモディファイアを使用することで、不要な処理の繰り返しを防ぎ、効率的なコードを書くことができます。特にデータ取得やアニメーションの初期化など、一度だけ実行したい処理に適しています。

SwiftUIのモディファイアシステムを拡張することで、コードの再利用性と可読性を高めることができました。ぜひあなたのプロジェクトでも活用してみてください!

GitHubで編集を提案

Discussion