I have been trying to reuse an Alert
from a BaseView
(SwiftUI) that can be shown by all View Models in my project so I can avoid boilerplate code.
For this I created two Views (BaseView
and BaseLoginView
) and two View Models (BaseViewModel
and LoginViewModel
)
I realized that you just can't create a BaseViewModel
and implement ObservableObject
because all @Published
variables on the child ViewModel will not fire any event so it seems that the ObservableObject
interface must be implemented on the final ViewModel, which in this case is LoginViewModel
.
But this leads to another problem: the BaseView
this way can't have an @ObservedObject var baseViewModel: BaseViewModel
because the BaseViewModel
doesn't conform to ObservableObject
. And the @ObservedObject
is needed to access the boolean bindings with $
.
This is what I a tried so far:
BaseView
struct BaseView<Content: View>: View {
@ObservedObject var baseViewModel: BaseViewModel
let content: Content
init(vm: BaseViewModel, @ViewBuilder content: () -> Content) {
self.baseViewModel = vm
self.content = content()
}
var body: some View {
VStack {
content
}
.alert(isPresented: self.$baseViewModel.showMessage) {
Alert(title: Text(self.baseViewModel.messageTitle), message: Text(self.baseViewModel.messageText), dismissButton: .default(Text("Okay")))
}
}
}
LoginView
-> Creates View Model and passes it to base Viewimport SwiftUI
struct LoginView: View {
@ObservedObject var loginViewModel: LoginViewModel
@State private var username: String = ""
@State private var password: String = ""
let textFieldHeight = CGFloat(60)
let textFieldPadding = CGFloat(15)
init() {
self.loginViewModel = LoginViewModel()
}
var body: some View {
ZStack {
NavigationView {
BaseView(vm: self.loginViewModel) {
VStack(alignment: .leading, spacing: 20) {
Text(self.loginViewModel.myTest)
}
Button(action: {
self.loginViewModel.login()
}) {
Text("Login")
}
}.navigationBarTitle("", displayMode: .inline)
}
}
}
}
BaseViewModel
import Foundation
class BaseViewModel : ObservableObject {
@Published var showMessage: Bool = false
@Published var messageTitle : String = ""
@Published var messageText : String = ""
public func showMessage(title: String, text: String) {
self.messageTitle = title
self.messageText = text
self.showMessage = true
}
}
LoginViewModel
import Foundation
class LoginViewModel : BaseViewModel {
@Published var myTest : String = "Hello"
func login() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.myTest = "Bye"
// self.showMessage(title: "Invalid Data", text: "Please fill all fields!")
}
}
}
My ContentView
that I created in a new project:
import SwiftUI
struct ContentView: View {
var body: some View {
//Text("Hello, World!")
LoginView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The problem is if I update the showMessage
it works but changing only the myTest
string to "Bye" doesn't update the view. It will only update if I also update a value that is published in the BaseViewModel
. Why is that?
How can I solve this problem and avoid boilerplate code?
You have to update myTest
property wrapper and forcefully call Base-class objectwillchange publisher.
@Published var myTest : String = "Hello" {
willSet {
self.objectWillChange.send()
}
}
I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.