This chapter introduces a basic class of elements in Combine: Transforming Operators. The conversion operator processes the value from the publisher into a format available to subscribers. You’ll notice similarities between the transform operators in Combine and the common operators in the Swift standard library, such as map and flatMap.

Operators are also Publishers

In Combine, we call the methods that perform operations on values from publishers “operators.” Each Combine operator returns a publisher. Typically, publishers receive upstream events, act on them, and then send the manipulated events downstream to consumers. To simplify this concept, in this chapter you will focus on using operators and processing their output.

Unless the purpose of the operator is to handle an upstream error, it simply republishes the said error downstream

Collect

Publisher that can publish individual values or collections of values. For example, use Publisher when you want to populate a list or grid view

function

The Collect operator provides a convenient way to convert a series of single values from a publisher into an array. To help understand this operator and all the other operators you’ll learn about in this book, you’ll use a pinball graph. The marble diagram helps to visualize how the operator works. The top line is the upstream publisher. The box represents the operator. The bottom line is what the subscriber, or more specifically, what the subscriber will receive after the operator manipulates the value from the upstream publisher. A baseline may also be another operator that receives output from an upstream publisher, performs its operations, and sends those values downstream.

Marble diagrams

Code sample

/ / sink receives the data 5 times [" A ", "B", "C", "D", "E"]. Publisher. Sink {print $0 ()} / / print A B C D E / / sink receives one data [" A ", "B", "C", "D", "E"]. Publisher. Collect (). The sink {print $0) (} / / print (" A ", "B", "C", "D", "E"] / / sink received 3 times (sequence number/collect element parameters, A remainder, add 1) [" A ", "B", "C", "D", "E"]. Publisher. Collect (2) sink {print ($0)} / / print (" A ", "B") (" C ", "D") (" E ")Copy the code

map

In addition to collecting data, you often want to transform it in some way. To do this, Combine provides a variety of mapping operators. The first thing you need to know is Map, which works like Swift’s standard Map, except that it operates on values sent by the publisher.

function

A map operates on a publisher’s Output and converts the data content.

For Output types that are primitives, you can get one of them as map(.x)

Marble diagrams

Code sample

[1, 2, 3, 4, Map {$0 *2}. Sink {print($0)} // print 2 4 6 8 10 // Output = URL(string: "https://www.baidu.com")! Let the pub = URLSession. Shared. DataTaskPublisher (url) that / / the original data, the reponse tuples, converted to a single data. The data map (\. Data)Copy the code

flatMap

FlatMap is one of the most powerful operators in Combine.

The definition is: flatMap operators flatten multiple upstream publishers into one downstream publisher — or, more specifically, flatten the emissions of those publishers.

In fact, flatMap can be simply interpreted as: The function of flatMap is to convert the upstream Publisher to the new Publisher. In general, the Output data type of the new Publisher can be different from the Output data type of the old Publisher, and the error type can also be different.

A common use case for flatMap in Combine is when you want to pass elements emitted by one publisher to a method that itself returns one publisher and ultimately subscribes to elements emitted by the second publisher.

function

FlatMap converts upstream publishers into new publishers.

Marble diagrams

Code sample

[1, 2, 3].publisher.flatmap ({int in // convert original publisher to new publisher // Output of new publisher to Range type return (0.. <int).publisher }).sink(receiveCompletion: { _ in }, receiveValue: { value in print("value: \(value)") })Copy the code

The output is

value: 0
value: 0
value: 1
value: 0
value: 1
value: 2
Copy the code

In practical applications, flatMap can be used in our search process. For example, we type a search keyword in the search bar and click the “Search” button, or click Return on the soft keyboard to search our keyword and Return the search results through network request.

You can see the following code:

ViewModel.swift

class ViewModel: ObservableObject {

    /// Search for the keyword Publisher to start the Combine process
    var searchPublisher = PassthroughSubject<String.Never> ()private var cancellable = Set<AnyCancellable> ()init(a) {
        
        searchPublisher
            .print("_Combine_")
			
            // MARK: - Subscribe to keyword changes through PassthroughSubject
            // Convert Publisher to Publisher
      ,>
            // Query the keyword with search
            .flatMap { searchContent in
                return self.search(searchContent)
            }
			
            .receive(on: RunLoop.main)
            .sink { [weak self] completion in
                switch completion {
                case .finished:
                    print("finished")
                case .failure(let error):
                    print("sink error: \(error)")
                }
            } receiveValue: { [weak self] searchResult in
                print("receiveValue: \(searchResult)")
                // We can store the @published variable in the ViewModel to refresh the View
            }
            .store(in: &cancellable)
    }
    
    /// Search by keyword and return to Publisher
    /// - Parameters:
    /// -text: keyword
    /// - Returns: AnyPublisher<StockInfo, Error>
    public func search(_ text: String) -> AnyPublisher<StockInfo.Error> {
        
        let url = URL(string: "your request url" + text)
        guard let url = url else {
            // URL failure returns an error Publisher
            return Fail(error: APIError.badURL).eraseToAnyPublisher()
        }
        
	// Network request Publisher
        return URLSession.shared.dataTaskPublisher(for: url)
            .retry(2)
            .map { $0.data }
            .decode(type: StockInfo.self, decoder: JSONDecoder())
            .replaceError(with: [])
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
	
	// Error enumeration definition
	enum APIError: Error.CustomStringConvertible {
   
        case badURL
        case badNetwork(error: Error)
        case badDecode
        case unknown

        var description: String {
            switch self {
            case .badURL:
                return "_URL Invalided_"
            case .badNetwork(let error):
                return "_network error: \(error.localizedDescription)_"
            case .badDecode:
                return "_decode error_"
            case .unknown:
                return "_unknown error_"}}}}Copy the code

In our View, we can start the search process by declaring the ViewModel variable and then calling searchPublisher.send in the ViewModel.

@StateObject var vm = ViewModel(a).

// called when searchingVm.searchpublisher.send (Search keywords)Copy the code

Without flatMap, we can imagine how to implement this search function.

  1. Declares a search function for making a web request, passing in the search keyword as an argument
  2. When the user starts a search, it retrieves the contents of the search box, and then calls the search function, passing in the search keyword as an argument

After using flatMap, the process becomes

  1. During initialization, create the Combine process

Search keywords change asynchronously — convert to network request — get search results

It’s just one step, and that’s the power of flatMap