Combine is a framework introduced by Apple in iOS 13 (as well as other platforms like macOS, watchOS, and tvOS) that provides a declarative Swift API for processing values over time. It simplifies working with asynchronous programming, making it easier to handle events, notifications, and data streams.

In this post, we will focus on Publishers when the source of data is a REST API. Specifically, we will implement two possible approaches using Publisher and Future, and discuss when it is better to use one over the other.

Starting Point for Base Code

The starting base code is the well-known Rick and Morty sample list-detail iOS app, featured in the DebugSwift post, Streamline Your Debugging Workflow.

From that point, we will implement the Publisher version, followed by the Future version. Finally, we will discuss the scenarios in which each approach is most suitable.

Publisher

Your pipeline always starts with a publisher, the publisher handles the “producing” side of the reactive programming model in Combine.

But lets start from the top view,  now  comment out previous viewmodel call for fetching data and call the new one based on Combine.

        .onAppear {
//            Task {
//                 await viewModel.fetch()
//            }
            viewModel.fetchComb()
        }

This is a fetch method for the view model, using the Combine framework:

import SwiftUI
@preconcurrency import Combine

@MainActor
final class CharacterViewModel: ObservableObject {
    @Published var characters: [Character] = []
    
    var cancellables = Set<AnyCancellable>()

       ...

    func fetchComb() {
        let api = CharacterServiceComb()
        api.fetch()
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("Fetch successful")
                case .failure(let error):
                    print("Error fetching data: \(error)")
                }
            }, receiveValue: { characters in
                self.characters = characters.results.map { Character($0) }
            })
            .store(in: &cancellables)
    }
}

The fetchComb function uses the Combine framework to asynchronously retrieve character data from an API service. It initializes an instance of CharacterServiceComb and calls its fetch() method, which returns a publisher. The function uses the sink operator to handle responses: it processes successful results by printing a message and mapping the data into Character objects, while logging any errors that occur in case of failure.

The subscription to the publisher is stored in the cancellables set, which manages memory and ensures the subscription remains active. When the subscription is no longer needed, it can be cancelled. This pattern facilitates asynchronous data fetching, error management, and updating the app’s state using Combine’s declarative style.

Let’s dive into CharacterServiceComb:

import Combine
import Foundation

final class CharacterServiceComb {

    let baseService = BaseServiceComb<ResponseJson<CharacterJson>>(param: "character")
    
    func fetch() -> AnyPublisher<ResponseJson<CharacterJson>, Error>  {
        baseService.fetch()
    }
}

Basically, this class is responsible for creating a reference to CharacterServiceComb, which is the component that actually performs the REST API fetch. It also sets up CharacterServiceComb for fetching character data from the service and retrieving a ResponseJson<CharacterJson> data structure.

Finally, CharacterServiceComb:

    func fetch() -> AnyPublisher<T, Error> {
        guard let url = BaseServiceComb<T>.createURLFromParameters(parameters: [:], pathparam: getPathParam()) else {
            return Fail(error: URLError(.badURL)).eraseToAnyPublisher()
        }
        
        return URLSession.shared.dataTaskPublisher(for: url)
            .map(\.data)
            .decode(type: T.self, decoder: JSONDecoder())
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }

It begins by constructing a URL using parameters and a path parameter. If the URL is valid, it initiates a network request using URLSession.shared.dataTaskPublisher(for:), which asynchronously fetches data from the URL. The response data is then mapped to a type T using JSONDecoder, and the result is sent to the main thread using .receive(on: DispatchQueue.main). Finally, the publisher is erased to AnyPublisher<T, Error> to abstract away the underlying types.

Finally, build and run the app to verify that it is still working as expected.

Future

The Future publisher will publish only one value and then the pipeline will close. When the value is published is up to you. It can publish immediately, be delayed, wait for a user response, etc. But one thing to know about Future is that it only runs one time.

Again lets start from the top view, comment out previous fetchComb and call the new one fetchFut based on Future

        .onAppear {
//            Task {
//                 await viewModel.fetch()
//            }
//            viewModel.fetchComb()
            viewModel.fetchFut()
        }

This is a fetch method for the view model that use the future:

import SwiftUI
@preconcurrency import Combine

@MainActor
final class CharacterViewModel: ObservableObject {
    @Published var characters: [Character] = []
    
    var cancellables = Set<AnyCancellable>()
        
    ...
    
    func fetchFut() {
        let api = CharacterServiceComb()
    api.fetchFut()
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("Fetch successful")
                case .failure(let error):
                    print("Error fetching data: \(error)")
                }
            }, receiveValue: { characters in
                self.characters = characters.results.map { Character($0) }
            })
            .store(in: &cancellables)
    }
}

This code defines a function fetchFut() that interacts with an API to fetch data asynchronously. It first creates an instance of CharacterServiceComb, which contains a method fetchFut() that returns a Future. The sink operator is used to subscribe to the publisher and handle its result. The receiveCompletion closure handles the completion of the fetch operation: it prints a success message if the data is fetched without issues, or an error message if a failure occurs.

The receiveValue closure processes the fetched data by mapping the results into Character objects and assigning them to the characters property. The subscription is stored in cancellables to manage memory and lifecycle, ensuring that the subscription remains active and can be cancelled if necessary.

final class CharacterServiceComb {

    let baseService = BaseServiceComb<ResponseJson<CharacterJson>>(param: "character")
    
    func fetch() -> AnyPublisher<ResponseJson<CharacterJson>, Error>  {
        baseService.fetch()
    }
    
    func fetchFut() -> Future<ResponseJson<CharacterJson>, Error> {
        baseService.fetchFut()
    }
}

The fetchFut() function now returns a Future instead of a Publisher.

Finally, CharacterServiceComb:

func fetchFut() -> Future<T, Error> {
        return Future { ( promise: @escaping (Result<T, Error>) -> Void) in
            nonisolated(unsafe) let promise = promise

                guard let url = BaseServiceComb<T>.createURLFromParameters(parameters: [:], pathparam: self.getPathParam())else {
                    return promise(.failure(URLError(.badURL)))
                }

                let task = URLSession.shared.dataTask(with: url) { data, response, error in
                    Task { @MainActor in
                        guard let httpResponse = response as? HTTPURLResponse,
                              (200...299).contains(httpResponse.statusCode) else {
                            promise(.failure(ErrorService.invalidHTTPResponse))
                            return
                        }
                        
                        guard let data = data else {
                            promise(.failure(URLError(.badServerResponse)))
                            return
                        }
                        
                        do {
                            let dataParsed: T = try JSONDecoder().decode(T.self, from: data)
                            promise(.success(dataParsed))
                        } catch {
                            promise(.failure(ErrorService.failedOnParsingJSON))
                            return
                        }
                    }
                }
                task.resume()
        }
    }
 

The provided code defines a function fetchFut() that returns a Future object, which is a type that represents a value that will be available in the future. It takes no input parameters and uses a closure (promise) to asynchronously return a result, either a success or a failure. When the URL is valid, then, it initiates a network request using URLSession.shared.dataTask to fetch data from the generated URL.

Once the network request completes, when the response is valid  data is received, it attempts to decode the data into a specified type T using JSONDecoder. If decoding is successful, the promise is resolved with the decoded data (.success(dataParsed)), otherwise, it returns a parsing error. The code is designed to work asynchronously and to update the UI or handle the result on the main thread (@MainActor). This is perfomed in that way becasue future completion block is exectued in main thread, so for still woring with promise we have to force to continue task in @MainActor.

Publisher vs Future

In iOS Combine, a Future is used to represent a single asynchronous operation that will eventually yield either a success or a failure. It is particularly well-suited for one-time results, such as fetching data from a network or completing a task that returns a value or an error upon completion. A Future emits only one value (or an error) and then completes, making it ideal for scenarios where you expect a single outcome from an operation.

Conversely, a Publisher is designed to handle continuous or multiple asynchronous events and data streams over time. Publishers can emit a sequence of values that may be finite or infinite, making them perfect for use cases like tracking user input, listening for UI updates, or receiving periodic data such as location updates or time events. Unlike Futures, Publishers can emit multiple values over time and may not complete unless explicitly cancelled or finished, allowing for more dynamic and ongoing data handling in applications.

Conclusions

In this exaple is clear that better approach is Future implementation.You can find source code used for writing this post in following repository.

References

Copyright © 2024-2025 JaviOS. All rights reserved