Integrate Combine Into an App

Aug 5 2021 · Swift 5.4, macOS 11.3, Xcode 12.5

Part 1: Define a View Model

03. Create Data Publishers

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 02. Use @Published to Publish State Next episode: 04. Use Publishers in the ViewModel

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

Transcript: 03. Create Data Publishers

Before we can use publishers in our view model, we have to define them first. We’ll need to use them in our production and test code, so some overarching protocols will work best here.

Then we’ll need publishers that will generate Jokes for both production and test,

as well as their Translations, again, in production and test. Let’s go to the code and define some protocols, and implement the necessary services.

Go to the Protocols folder and select the JokeServiceDataPublisher.swift file, and you’ll see an empty JokeServiceDataPublisher protocol. Add a publisher function that returns an AnyPublisher that takes in Data and returns a URLError

public protocol JokeServiceDataPublisher {
  func publisher() -> AnyPublisher<Data, URLError>
}

In the same folder, go to TranslationServiceDataPublisher.swift file, and add a publisher function to this protocol.

public protocol TranslationServiceDataPublisher {
  func publisher(for joke: Joke, to languageCode: String)
    -> AnyPublisher<Data, URLError>
}

This publisher looks similar in form to the JokeServiceDataPublisher publisher, but takes in a Joke and a String representing the language code.

The return types for these two functions indicate we’ll be using eraseToAnyPublisher at the end of the Combine pipeline we’ll build.

Speaking of which, go to the Services folder and select the TranslationService.swift file and add an extension so TranslationService will adopt TranslationServiceDataPublisher

extension TranslationService: TranslationServiceDataPublisher {
  public func publisher(for joke: Joke, to languageCode: String)
    -> AnyPublisher<Data, URLError> {
    URLSession.shared.dataTaskPublisher(
      for: url(for: joke, languageCode: languageCode)
    )
    .map(\.data)
    .eraseToAnyPublisher()
  }
}

In this function, a dataTaskPublisher is formed from the joke and languageCode using a helper method found earlier in the file. The map operator is used to grab the data portion of the returned tuple, and eraseToAnyPublisher is used to wrap the publisher in an instance of AnyPublisher, which hides the true nature of the underlying publisher.

Do something similar inside the Services folder for JokeService.swift

extension JokesService: JokeServiceDataPublisher {
  public func publisher() -> AnyPublisher<Data, URLError> {
    URLSession.shared
      .dataTaskPublisher(for: url)
      .map(\.data)
      .eraseToAnyPublisher()
  }
}

This publisher uses the private property of the JokesService to get a random joke from the joke service URL. map is again used to grab the data portion of the tuple, and eraseToAnyPublisher performs the type-erasure for us.

Now to the testing code. The starter code for the project has test structs that adopt the protocols we filled out earlier, but doesn’t have the implementations of the methods yet.

In the ChuckNorrisJokesTests/Services/MockJokesService.swift file, implement the publisher function, but here just use a CurrentValueSubject. If the Mock service is initialized with an error, send the completion with a failure; otherwise just call eraseToAnyPublisher on the CurrentValueSubjectPublisher and return it.

func publisher() -> AnyPublisher<Data, URLError> {
  // 1
  let publisher = CurrentValueSubject<Data, URLError>(data)
  
  // 2
  if let error = error {
    publisher.send(completion: .failure(error))
  }
  
  // 3
  return publisher.eraseToAnyPublisher()
}

In ChuckNorrisJokesTests/Services/MockTranslationService.swift, do the same thing, but in the error case, use DispatchQueue.global().asyncAfter to wait a tenth of a second before sending the failing completion event. The delay here will be used when performing unit tests later on.

func publisher(for joke: Joke, to languageCode: String) -> AnyPublisher<Data, URLError> {
  // 1
  let publisher = CurrentValueSubject<Data, URLError>(data)
  
  // 2
  if let error = error {
    DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) {
      publisher.send(completion: .failure(error))
    }
  }
  
  // 3
  return publisher.eraseToAnyPublisher()
}

OK, our publishers are in place, so let’s use them - first, in the view model, which we’ll do in the next episode.