Run a method after SwiftUI View updates

Hello CWC Community,

I want to run a method once a SwiftUI View updates after a published property changes.

  1. For example, I have a view that has an @ObservedObject property named randomViewModel.
struct ContentView: View {
    @ObservedObject var randomViewModel: RandomViewModel

    var body: some View {
        Text("Hello World!")
            .background(Color.random())
    }
}
  1. randomViewModel is an instance of RandomViewModel which has a published property called randomNumber:
class RandomViewModel: ObservableObject {
    @Published public var randomNumber: Int
    
    init(randomNumber: Int = 1) {
        self.randomNumber = randomNumber
    }
}
  1. The randomNumber published property is updated when the user taps on an instance of this struct.
struct HelperView: View {
    var randomViewModel: RandomViewModel
    var body: some View {
        Text("Hi")
            .onTapGesture {
                randomViewModel.randomNumber = randomViewModel.randomNumber + 1
            }
    }
}
  1. When ContentView renders from the published property randomNumber changing, I want to run some code/method. For example:
func printHappyBirthday() {
    print("Happy Birthday!")
}

How can I do this? I have tried to add the method printHappyBirthday() to onAppear() but onAppear() doesn’t get triggered after the SwiftUI view is updated:

struct ContentView: View {
    @ObservedObject var randomViewModel: RandomViewModel

    var body: some View {
        Text("Hello World!")
            .background(Color.random())
            .onAppear {
               printHappyBirthday()
            }
    }
}

Thank you.

The modifier you want is

.onChange(of: yourPublishedProperty) { newValue in
    // Your action code here. 
}

and then do something with it in the closure.

Thank you so much!!

1 Like

Can’t you just add the action to the .onTapGesture code?

@siriswag

When I posted my reply yesterday I was on my iPhone and I didn’t get to have a detailed look at your code you provided.

In this scenario you have provided, is the HelperView part of the View hierarchy in the project you are working with?
If it is, then how are you providing access to the ObservableObject? Are you passing it in from the parent View and then passing that to the HelperView?

Hi Chris, this is how I am providing access to the ObservableObject.

@main
struct PlayingWithPublishedApp: App {
    let persistenceController = PersistenceController.shared
    let randomViewModel: RandomViewModel = RandomViewModel()

    var body: some Scene {
        WindowGroup {
            ContentView(randomViewModel: randomViewModel)
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
            HelperView(randomViewModel: randomViewModel)
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
        }
    }
}

Hi @PeteTheGolfer, yes in this scenario I can!

@siriswag

In the code above, RandomViewModel is not being injected into the view Hierarchy. You’ve defined it as randomViewModel but that’s not enough to make it useable.

Also you are injecting a separate instance of the managedObjectContext into ContentView and HelperView.

The other point is that you have two views in WindowGroup. Why?

The accepted way to structure a project is that WindowGroup only calls one View which is the entry point to your App. If anything what you should do is this:

@main
struct siriswagApp: App {
    let randomViewModel = RandomViewModel()
    let persistenceController = PersistenceController.shared
    
    var body: some Scene {
        WindowGroup {
            MainView()
                .environment(randomViewModel)
                .environment(persistenceController)
        }
    }
}

MainView (Note: I have not included any reference to your core data entity in subsequent Views).

struct MainView: View {
    var body: some View {
        VStack(spacing: 30) {
            ContentView()
            HelperView()
        }
    }
}

#Preview {
    MainView()
        .environment(RandomViewModel())
}

ContentView

struct ContentView: View {
    @Environment(RandomViewModel.self) var randomViewModel

    var body: some View {
        VStack(spacing: 30) {
            Text("Hello World!")
                .background(Color.green)
            Text("RandomNumber value: \(randomViewModel.randomNumber)")
        }
        .onChange(of: randomViewModel.randomNumber) { oldValue, newValue in
            printHappyBirthday()
        }

    }

    func printHappyBirthday() {
        print("Happy Birthday!")
    }
}

#Preview {
    ContentView()
        .environment(RandomViewModel())
}

HelperView

struct HelperView: View {
    @Environment(RandomViewModel.self) var randomViewModel

    var body: some View {
        Text("Hi")
            .onTapGesture {
                randomViewModel.randomNumber = randomViewModel.randomNumber + 1
                print("Random number updated")
            }
    }
}

#Preview {
    HelperView()
        .environment(RandomViewModel())
}

RandomViewModel

// This conforms to the new iOS17 Observable Macro which means that 
// randomNumber is effectively a Published property
@Observable  
class RandomViewModel {
     var randomNumber: Int

    init(randomNumber: Int = 1) {
        self.randomNumber = randomNumber
    }
}

I hope some of that helps.

Yes! This helps. Thank you so much!!