Search code examples
swiftmvvmcombinepublisher

Bind the view with result of multiple publishers


I am new to Combine and trying to use it in my application for validation of a form. It is a typical form with first name, last name, email and phoneNumber. There will be a submit button which will be initially disabled and will become active once the validations are passed.

Following is my UIViewController code:

firstNameTextView.text = dependencies.leadConsumptionUseCase.firstName
    lastNameTextView.text = dependencies.leadConsumptionUseCase.lastName
    phoneNumberTextView.text = dependencies.leadConsumptionUseCase.phoneNumber
    emailTextView.text = dependencies.leadConsumptionUseCase.emailAddress

    self.cancellable = dependencies.leadConsumptionUseCase.isSignupFormValidPublisher.receive(on: RunLoop.main).sink(receiveValue: { isValid in
        self.continueButton.isEnabled = isValid
    })

Viewmodel Protocol:

    public protocol LeadConsumptionProtocol {
    func setLead(_ lead: Lead)

    func saveLead(leadConsumptionDetails: Lead) -> AnyPublisher<Void, Error>
    var isSignupFormValidPublisher: AnyPublisher<Bool, Never> { get }
    
    var firstName: String { get set}
    var lastName: String { get set}
    var phoneNumber: String { get set}
    var emailAddress: String { get set}
}

ViewModel/Interactor code:

@Published public var firstName = ""
    @Published public var lastName = ""
    @Published public var phoneNumber = ""
    @Published public var emailAddress = ""
var isFirstNameValidPublisher: AnyPublisher<Bool, Never> {
        $firstName
            .removeDuplicates()
            .map { name in
                return name.count >= 3
            }
            .eraseToAnyPublisher()
    }
    
    var isLastNameValidPublisher: AnyPublisher<Bool, Never> {
        $lastName
            .removeDuplicates()
            .map { name in
                return name.count >= 3
            }
            .eraseToAnyPublisher()
    }
    
    var isUserEmailValidPublisher: AnyPublisher<Bool, Never> {
          $emailAddress
            .removeDuplicates()
              .map { email in
                  let emailPredicate = NSPredicate(format:"SELF MATCHES %@", "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}")
                  return emailPredicate.evaluate(with: email)
              }
              .eraseToAnyPublisher()
      }
      
      var isPhoneNumberValidPublisher: AnyPublisher<Bool, Never> {
          $phoneNumber
              .removeDuplicates()
              .map { phoneNumber in
                  return phoneNumber.count >= 8
              }
              .eraseToAnyPublisher()
      }
    
    public var isSignupFormValidPublisher: AnyPublisher<Bool, Never> {
        Publishers.CombineLatest4(
            isFirstNameValidPublisher,
            isLastNameValidPublisher,
            isPhoneNumberValidPublisher,
            isUserEmailValidPublisher)
          .map { isNameValid, isEmailValid, isPasswordValid, passwordMatches in
              return isNameValid && isEmailValid && isPasswordValid && passwordMatches
          }
          .eraseToAnyPublisher()
      }

I assume that binding is not working properly. What can I try next?


Solution

  • Your ValidPublishers are computed properties, meaning they'll create new Publishers every time you access them.

    When you mark a property using @Published, you can access that property's publisher using the $ sign (i.e: $firstName) then in the map you return your logic:

    public var isSignupFormValidPublisher: AnyPublisher<Bool, Never>
    

    And in the viewModel init:

    isSignupFormValidPublisher = Publishers.Zip4(//Zip to listen to any Publisher output, unlike CombineLatest which requires all of them to change
            $firstName
            $lastName,
            $phoneNumber,
            $emailAddress)
        .map { firstName, lastName, phoneNumber, email in
            return yourLogic
        }.eraseToAnyPublisher()