everyone,

I'm currently working on a SwiftUI project and have encountered a question regarding the usage of @Published in a view model. I have a login view with a variable 'errorMessage' in the view model.

I noticed that even when I removed @Published from the variable, the app still functions correctly. I have injected the view model into the SwiftUI environment using @EnvironmentObject, but I haven't explicitly marked the variable with @Published or used objectWillChange.send() in the view model.

My question is: Under this scenario, should I still use @Published explicitly when a property needs to be observed and trigger view updates? What could be the difference between declaring a variable with @Published and without it in this context? Are there any best practices or potential implications that I should be aware of?

I would greatly appreciate any insights, suggestions, or examples related to this topic. Thank you in advance for your help!

import SwiftUI

struct LoginView: View {
    @EnvironmentObject var authService: AuthService
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        ZStack {
            Text(authService.errorMessage)
        }
        .onSubmit {
            Task {
                if await authService.signIn(){
                     dismiss()
                }
            }
        }
    }
}

import Foundation
import FirebaseAuth

enum AuthError: Error {
    case emptyPassword
    case emptyEmail
}

extension AuthError: LocalizedError {
    var errorDescription: String? {
        switch self {
        case .emptyPassword:
            return NSLocalizedString("Please enter a password to continue.", comment: "")
        case .emptyEmail:
            return NSLocalizedString("Please enter an email address to continue.", comment: "")
}

@MainActor
final class AuthService: ObservableObject {
    @Published var email = ""
    @Published var password = ""
    
    private(set) var errorMessage = ""

    private func validation() throws {
        if email.isEmpty {
            throw AuthError.emptyEmail
        }
        if password.isEmpty {
            throw AuthError.emptyPassword
        }
    }
    
    func signIn() async -> Bool {
        do {
            try validation()
            let authResult = try await auth.signIn(withEmail: email, password: password)
            return true
        } catch {
            errorMessage = error.localizedDescription
            return false
        }
    }
}

I have examined the pertinent concepts (Published, EnvironmentObject, ObservableObject etc.) within the documentation provided by Apple.

1

There are 1 best solutions below

1
On BEST ANSWER

Here are a few best practices:

  1. Use the View struct and property wrappers instead of a legacy view model object, this increases performance and prevents consistency bugs. let in a View struct is the simplest feature: body is called when the let value changes from the last time the View was init. It's inefficient to re-implement this basic pattern yourself with @Published in ObservableObject from the Combine framework.
  2. Use .task and .task(id: signIn) instead of Combine's ObservableObject when using async/await, this gives you cancellation and restart features.
  3. Use a custom EnvironmentKey for a service, like how AuthorizationController is implemented.