Combine and Clean Architecture

Hi everyone. I’m learning a lot of from Combine thanks to this amazing book.
Currently i’m writing an app and following the clean MVVM architecture.

Then I have the following Layers:

  • Domain(UseCases and RepositoryProtocols)
  • Data(RepositoryImplementations)
  • Presentation(ViewController and ViewModels)

So this is what I have by now:

LoginUseCase.swift

import Foundation
import Combine
public enum LoginState {
    case idle
    case inProgress
    case success
    case emailValidationPending
    case wrongPassword
    case invalidEmail
    case invalidPassword
    case error
}
public protocol LoginUseCase {
    func login(email: String, password: String) -> AnyPublisher<LoginState, Never>
}
public class LoginUseCaseImp: LoginUseCase {
    let repository: RegistroRepository
    public init(repository: RegistroRepository) {
        self.repository = repository
    }
    public func login(email: String, password: String) -> AnyPublisher<LoginState, Never> {
        return Future<LoginState, Never> { [weak self] promise in
            self?.repository.login(email: email, password: password) { (result: Result<Bool, LoginRepositoryError>) in
                switch result {
                case .success(_):
                    return promise(.success(.success))
                case .failure(let error):
                    switch error {
                    case .wrongPassword:
                        return promise(.success(.wrongPassword))
                    case .userNotExist:
                        return promise(.success(.error))
                    case .emailValidationPending:
                        return promise(.success(.emailValidationPending))
                    }
                }
                
            }
        }.eraseToAnyPublisher()
    }
}

LoginViewModel.swift

import Foundation
import DomainRadarAir
import Combine

public final class LoginViewModel: ObservableObject {
    private let loginUseCase: LoginUseCase
        
    public init(loginUseCase: LoginUseCase) {
        self.loginUseCase = loginUseCase
    }
    
    public func login(email: String?, password: String?) -> AnyPublisher<LoginState, Never> {
        guard let email=email, !email.isEmpty else {
            return Just<LoginState>(.invalidEmail).eraseToAnyPublisher()
        }
        guard let password=password, !password.isEmpty else {
            return Just<LoginState>(.invalidPassword).eraseToAnyPublisher()
        }
        
        return loginUseCase.login(email: email, password: password)
    }
}

Then in the LoginViewController.swift

    @IBAction func loginClick(_ sender: Any) {
            viewModel.login(email: emailTF.text, password: passwordTF.text)
                .print("State: ")
                .sink { [weak self] state in
                    guard let self = self else { return }
                    switch state {
                    // all cases
                    }
                .store(in: &cancellables)
    }

What do you think of this approach, passing AnyPublisher object from UseCase to ViewController through ViewModel?

Thanks in advance for your opinions.

I’m not a big fan of Clean nor MVVM architecture, because it makes so much boilerplate code. I would just go with MVC + Extensions + Combine. Less code, easier to read and maintain.

Thanks for the response. For sure MVC is a valid approach as well, but my goal is using MVVM + clean in a new project.