Search code examples
swiftmvvmcombineflatmappublisher

Use result of publisher in map of another publisher


As example I have a basic published value like

@Published var value: String

I have want to validates this value of my form to give my user an output. For that I will use Combine in my MVVM project.

Now this type of value needs to be validated against my REST API. For my REST API I already have a method to get my results of my like getFirstMailboxRedirectFromAPI which returns AnyPublisher<MailboxAPIResponse, APIError>. MailboxAPIResponse is a decodable object for the api response. So if I just want to display the result, I create a subscriber with .sink add the result to a variable which will be shown in a view. So good so far. Now my problem:

As described in the first section my value is already a Publisher (because of @Published), where I can do some .map stuff for validation with it and returning true or false if everything is fine or not.

So to validate my published value I need to call my other Publisher which uses the API to check if the value already exists. But I don't know how this should work.

This is my code so far but this doesn't work. But this shows you my idea how it should work.

private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
    $mailboxRedirectName
        .debounce(for: 0.5, scheduler: RunLoop.main)
        .setFailureType(to: Error.self)
        .flatMap { name in
            self.getFirstMailboxRedirectFromAPI(from: name, and: self.domainName)
                .map { apiResponse in
                    return apiResponse.response.data.count == 0
                }
        }
        .eraseToAnyPublisher()
}

So in result the publisher should use the redirectName to call the API and the API gives me the result if the mailbox already exists, then returns a boolean, if it's existing or not.

How can I nest multiple publishers and use the result of the API publisher in my published value publisher?


Solution

  • I simplified a little but key take aways are 1) use switchToLatest to flatten a Publisher of Publishers if you want the operation to restart (flatMap is a merge, so events could arrive out of order). 2) You need to handle the failure types and make sure an inner Publisher never fails or the outer publisher will also complete.

    final class M: ObservableObject {
      @Published var mailboxRedirectName: String = ""
      private var isMailboxRedirectNameAvailablePublisher: AnyPublisher<Bool, Never> {
          $mailboxRedirectName
            .debounce(for: 0.5, scheduler: DispatchQueue.main)
              .map { [weak self] name -> AnyPublisher<Bool, Never> in
                guard let self = self else { return Just(false).eraseToAnyPublisher() }
                return self
                  .getFirstMailboxRedirectFromAPI(from: name)
                  .replaceError(with: false)
                  .eraseToAnyPublisher()
              }
              .switchToLatest()
              .eraseToAnyPublisher()
      }
    
      func getFirstMailboxRedirectFromAPI(from name: String) -> AnyPublisher<Bool, Error> {
        Just(true).setFailureType(to: Error.self).eraseToAnyPublisher()
      }
    }