1

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 :)

Eric
  • 592
  • 10
  • 26

4 Answers4

1

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
}
Yrb
  • 8,103
  • 2
  • 14
  • 44
  • Thanks for advice on await end onReceive, good thing to know. But I stay with a pat of my problem, each time I come on the page, onAppear is executed and also the compute. So I found here (https://stackoverflow.com/questions/56496359/swiftui-view-viewdidload) a way to execute the initial setup only once, and then onReceive when data changes. So all good – Eric Apr 01 '22 at 15:15
1

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.

Namra Parmar
  • 227
  • 2
  • 6
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 01 '22 at 09:09
  • Didn't test it but the workaround seems ok. I prefer the .onReceive() solution as settings a kind of global variable to deal with a local problem seems less "clean". Thanks for the idea anyway, can be useful somewhere else. – Eric Apr 01 '22 at 15:21
1

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.

malhal
  • 26,330
  • 7
  • 115
  • 133
1

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

Zera Zinc
  • 29
  • 4