Search code examples
swiftswiftuiobservableobject

How to show/reuse the same view (Alert) in SwiftUI from all ViewModels in case of an error/message?


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:

1 - 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 View

import 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?


Solution

  • You have to update myTest property wrapper and forcefully call Base-class objectwillchange publisher.

    @Published var myTest : String = "Hello" {
            willSet {
                self.objectWillChange.send()
            }
        }
    

    Note: @Published property wrapper is not working incase of inherited class.

    I hope this is something that will be fixed in future releases as the objectWillChange.send() is a lot of boilerplate to maintain.