I find myself with several Alert boxes in my app for various purposes. In pursuit of the goal of DRY (don't repeat yourself), I want to centralize this code. My naieve approach is to write a ViewModel that controls Alert state, both content and whether or not it's showing. That code is simple
@MainActor
class AlertViewModel: ObservableObject {
@Published var showAlert = false
@Published var title: String = "Default Alert title"
@Published var message: String = "Default Alert Message"
}
Then you can use it like this
struct MainView: View {
@StateObject var vm = AlertViewModel()
var body: some View {
Button("Show alert on main view") {
vm.showAlert = true
}
.alert(isPresented: $vm.showAlert) {
Alert(title: Text(vm.title), message: Text(vm.message))
}
}
}
And it works beautifully. However, using it from a subview doesn't work. Here's the above code combined with a subview
struct SubView: View {
@StateObject var vm = AlertViewModel()
var body: some View {
Button("Show alert on sub view") {
vm.showAlert = true
}
}
}
struct MainView: View {
@StateObject var vm = AlertViewModel()
var body: some View {
SubView()
Button("Show alert on main view") {
vm.showAlert = true
}
.alert(isPresented: $vm.showAlert) {
Alert(title: Text(vm.title), message: Text(vm.message))
}
}
}
The subview button doesn't cause the alert to show.
Why?
Is there a workaround?
Am I needlessly chasing code minimalism?
What's the correct approach here?
When a MainView is first created @StateObject will instantiate the AlertViewModel. In SubView you have used @StateObject, this will create another instance of AlertViewModel. There are two different AlertViewModel instances, that is why the alert did not show when button is clicked in SubView.
To use the same instance from MainView, you should pass the AlertViewModel
SubView(vm: vm)
In SubView use ObservedObject, if you want the SubView to update if any published property of AlertViewModel changes
@ObservedObject var vm: AlertViewModel
or else you can use var vm: AlertViewModel
try the below code body in SubView to check the difference, with and without @ObservedObject
var body: some View {
Button("Show alert on sub view one") {
vm.showAlert = true
}
Rectangle()
.foregroundStyle(vm.showAlert ? Color.red : Color.green)
}
If you want to pass AlertViewModel to random subView without passing AlertViewModel to all subView like a chain, you can use environment Object
struct MainView: View {
@StateObject var vm = AlertViewModel()
var body: some View {
VStack {
SubViewOne()
Button("Show alert on main view") {
vm.showAlert = true
}
.alert(isPresented: $vm.showAlert) {
Alert(title: Text(vm.title), message: Text(vm.message))
}
}
.environmentObject(vm)
}
}
struct SubViewOne: View {
var body: some View {
SubViewTwo()
}
}
struct SubViewTwo: View {
@EnvironmentObject var vm: AlertViewModel
var body: some View {
Button("Show alert on sub view two") {
vm.showAlert = true
}
}
}