How to conditionally execute code on onAppear method

1.9k Views Asked by At

I have a swiftUi view depending on a class data. Before displaying the data, I have to compute it in .onAppear method.

I would like to make this heavy computation only when my observed object changes.

The problem is that .onAppear is called every time I open the view, but the object value does not change very often.

Is it possible to conditionally execute the compute function, only when observed data has effectively been modified ?

import SwiftUI
struct test2: View {
    @StateObject var person = Person()
    @State private var computedValue = 0
    
    var body: some View {
        
        List {
            Text("age = \(person.age)")
            Text("computedValue = \(computedValue)")
        }
        .onAppear {
            computedValue = compute(person.age)     /// Executed much too often :(
        }
    }
    
    func compute(_ age: Int) -> Int {
        return age * 2  /// In real life, heavy computing
    }
}

class Person: ObservableObject {
    var age: Int = 0
}

Thanks for advice :)

4

There are 4 best solutions below

0
On

task(id:priority:_:) is the solution for that.

"A view that runs the specified action asynchronously when the view appears, or restarts the task with the id value changes."

Set the id param to the data you want to monitor for changes.

2
On

A possible solution is that create a EnvironmentObject with a Bool Value, Change The value of that variable when there are Change in your object. So onappear just check if environmentobject value is true or false and it will execute you function. Hope You Found This Useful.

1
On

It would probably be a little less code in the view model, but to do all of the calculations in the view, you will need a few changes. First, your class Person has no @Published variables, so it will never call for a state change. I have fixed that.

Second, now that your state will update, you can add an .onReceive() to the view to keep track of when age updates.

Third, and extremely important, to keep from blocking the main thread with the "heavy computing", you should implement Async Await. As a result, even though I sleep the thread for 3 seconds, the UI is still fluid.

struct test2: View {
    @StateObject var person = Person()
    @State private var computedValue = 0
    
    var body: some View {
        
        List {
            Text("age = \(person.age)")
            Text("computedValue = \(computedValue)")
            Button {
                person.age = Int.random(in: 1...80)
            } label: {
                Text("Randomly Change Age")
            }

        }
        // This will do your initial setup
        .onAppear {
            Task {
                computedValue = await compute(person.age)     /// Executed much too often :(
            }
        }
        // This will keep it current
        .onReceive(person.objectWillChange) { _ in
            Task {
                computedValue = await compute(person.age)     /// Executed much too often :(
            }
        }
    }
    
    func compute(_ age: Int) async -> Int {
        //This is just to simulate heavy work.
        do {
            try await Task.sleep(nanoseconds: UInt64(3.0 * Double(NSEC_PER_SEC)))
        } catch {
            //handle error
        }
        return age * 2  /// In real life, heavy computing
    }
}

class Person: ObservableObject {
    @Published var age: Int = 0
}
0
On

I was running into the same issue. I use a .task view function to fetch backend data. And the problem is .task gets executed every time when I switch Views. (My App has a TabView with multiple tabs). The easiest way is to introduce a @State variable to track the data loading status. Below is the solution:

import SwiftUI
struct test2: View {
    @StateObject var person = Person()
    @State private var computedValue = 0
    @State private var isDataLoaded = false
    
    var body: some View {
        
        List {
            Text("age = \(person.age)")
            Text("computedValue = \(computedValue)")
        }
        .onAppear {
            if isDataLoaded {
               return
            } 
            computedValue = compute(person.age)     /// Executed much too often :(
            isDataLoaded = true
        }
    }
    
    func compute(_ age: Int) -> Int {
        return age * 2  /// In real life, heavy computing
    }
}

class Person: ObservableObject {
    var age: Int = 0
}

I am not sure if this is a good practice but it somehow solved my problem. Hope it can help

reference: https://forums.swift.org/t/how-to-launch-effect-onapper-only-once/63455/4