Etiqueta: Combine

  • Daily Swift Combine by example

    Daily Swift Combine by example

    The aim of this post is to get introduced to Combine by comparing it with regular operations that we perform on a daily basis while programming in Swift. It focuses on illustrating how common imperative patterns can be re-expressed using a reactive and declarative approach.

    From Callback to Combine

    Using a traditional completion handler is straightforward and lightweight. It works well for single, isolated asynchronous operations where you just need to return a Result once and move on. It introduces no dependency on Combine and keeps the control flow explicit and easy to follow. However, composition becomes cumbersome as soon as you need to chain multiple async steps, handle retries, debounce input, combine multiple data sources, or coordinate cancellation. Cancellation and flow control must be designed manually, and complex logic can quickly devolve into nested closures or scattered error handling.

    struct User: Decodable {
        let id: Int
        let name: String
    }
    
    final class API {
        func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
            DispatchQueue.global().asyncAfter(deadline: .now() + 0.3) {
                completion(.success(User(id: id, name: "Ada")))
            }
        }
    }

    Wrapping the same API in a Combine Future turns the operation into a Publisher, which enables declarative composition with operators like map, flatMap, retry, and catch. This makes it much easier to build scalable, testable pipelines and maintain architectural consistency (e.g., in MVVM). The trade-offs are additional conceptual complexity, lifecycle management via AnyCancellable, and the fact that Future is eager and single-shot. Moreover, cancellation of a subscription does not automatically cancel the underlying work unless the original API supports it and you explicitly propagate that behavior.

    import Combine
    
    final class APICombine {
        private let api = API()
    
        func fetchUser(id: Int) -> AnyPublisher<User, Error> {
            Future { [api] promise in
                api.fetchUser(id: id) { result in
                    promise(result)
                }
            }
            .eraseToAnyPublisher()
        }
    }
    
    var cancellables = Set<AnyCancellable>()
    
    func fetchUser(id: Int) {
        let api = APICombine()
        api.fetchUser(id: 1)
            .sink(
                receiveCompletion: { completion in
                    if case let .failure(error) = completion {
                        print("Error:", error)
                    }
                },
                receiveValue: { user in
                    print("User:", user.name)
                }
            )
            .store(in: &cancellables)
    }

    Real Networking: URLSession.dataTaskPublisher

    Using URLSession.dataTaskPublisher provides a highly declarative and composable approach to networking. It integrates natively into the Combine pipeline, allowing you to chain operators such as map, tryMap, decode, retry, catch, and receive(on:) in a single, expressive stream. This makes transformation, error propagation, threading, and cancellation first-class concerns. It also improves testability when paired with dependency injection and custom URLSession configurations, and it aligns naturally with reactive UI layers (e.g., SwiftUI with @Published). The main advantages are composability, consistency with reactive architecture, built-in cancellation via AnyCancellable, and clearer data-flow semantics.

    import Foundation
    import Combine
    
    struct Post: Decodable {
        let id: Int
        let title: String
    }
    
    enum APIError: Error {
        case badStatus(Int)
    }
    
    final class PostsService {
        func fetchPosts() -> AnyPublisher<[Post], Error> {
            let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    
            return URLSession.shared.dataTaskPublisher(for: url)
                .tryMap { output -> Data in
                    if let http = output.response as? HTTPURLResponse,
                       !(200...299).contains(http.statusCode) {
                        throw APIError.badStatus(http.statusCode)
                    }
                    return output.data
                }
                .decode(type: [Post].self, decoder: JSONDecoder())
                .eraseToAnyPublisher()
        }
    }

    However, dataTaskPublisher introduces additional abstraction and complexity compared to a traditional completion-handler approach. The generic types in Combine pipelines can become difficult to read, often requiring eraseToAnyPublisher() to manage API surface complexity. Debugging reactive chains can also be more challenging due to operator layering and asynchronous propagation. Moreover, for simple one-off requests, a completion handler or even Swift’s modern async/await syntax may be more readable and straightforward. Combine shines in scenarios involving multiple asynchronous streams, transformation pipelines, or UI bindings, but it may feel unnecessarily heavy for basic networking tasks.

    let service = PostsService()
    
    func PostServiceUsage() {
    
        service.fetchPosts()
            .receive(on: DispatchQueue.main) // UI updates
            .sink(
                receiveCompletion: { print("Completion:", $0) },
                receiveValue: { posts in print("Posts:", posts.count) }
            )
            .store(in: &cancellables)
    }

    NotificationCenter in Combine

    Using NotificationCenter with the classic addObserver API is straightforward and has minimal abstraction overhead. It works well for simple, fire-and-forget events and does not require knowledge of reactive programming. However, it is loosely typed (the notification’s payload is typically extracted from userInfo), which increases the risk of runtime errors. It also requires manual lifecycle management: you must ensure observers are removed appropriately (unless relying on the block-based API introduced in iOS 9+), and there is no built-in way to declaratively transform, filter, or compose events. As the number of notifications grows, code can become fragmented and harder to reason about.

    import Foundation
    import UIKit
    
    func classicUsage() {
        NotificationCenter.default.addObserver(
            forName: UIApplication.didBecomeActiveNotification,
            object: nil,
            queue: .main
        ) { _ in
            print("App activa")
        }
    }

    The Combine-based NotificationCenter.Publisher approach integrates notifications into a reactive stream, enabling strong composition through operators like map, filter, debounce, and merge. This allows you to declaratively transform and coordinate notification-driven events within a unified asynchronous pipeline. Memory management is also more explicit and predictable via AnyCancellable, and cancellation semantics are clearer. The trade-offs are increased conceptual complexity and a steeper learning curve, particularly for teams unfamiliar with reactive paradigms. Additionally, for very simple use cases, Combine may introduce unnecessary abstraction compared to the traditional observer pattern.

    func combineUsage() {
        NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
            .sink { _ in
                print("App active")
            }
            .store(in: &cancellables)
    }
    

    Timer as a Publlisher

    Using Timer.publish(every:on:in:) from Combine provides a declarative, composable approach to time-based events. Instead of imperatively scheduling a Timer and manually invalidating it, you model time as a stream of values that can be transformed with operators like map, scan, throttle, or debounce. This makes it especially powerful when the timer is just one part of a larger reactive pipeline (e.g., polling a network endpoint, driving UI state, or coordinating multiple asynchronous streams). Memory management is also more uniform: cancellation is handled via AnyCancellable, which integrates naturally with other Combine subscriptions. The main advantage is composability and consistency within a reactive architecture.

    import Combine
    
    func setupTimer() {
        Timer.publish(every: 1.0, on: .main, in: .common)
            .autoconnect()
            .scan(0) { count, _ in count + 1 }  // contador
            .sink { value in
                print("Segundos:", value)
            }
            .store(in: &cancellables)
    }

    In contrast, the traditional Timer.scheduledTimer approach is simpler and often more readable for isolated, imperative use cases—particularly when you just need a repeating callback with minimal transformation logic. It has less conceptual overhead and avoids introducing reactive abstractions where they may not be justified. However, it requires manual lifecycle management (invalidating the timer at the correct moment), is less expressive for chaining asynchronous behavior, and does not integrate naturally with other reactive data flows. Therefore, the imperative timer is often preferable for small, self-contained tasks, while the Combine publisher approach scales better in complex, event-driven architectures.

    Search with debounce

    Using a reactive approach with Combine (e.g., debounce, removeDuplicates, flatMap) centralizes the entire search pipeline into a single declarative data flow. The main advantage is composability: input normalization, rate limiting, cancellation of in-flight requests, error handling, and threading can all be expressed as operators in a predictable chain. This significantly reduces race conditions and makes it easier to reason about asynchronous behavior. Additionally, flatMap combined with switchToLatest (if used) allows automatic cancellation of outdated requests, which is critical in fast-typing scenarios. The architecture also scales well, especially in MVVM, because the ViewModel cleanly separates input streams from output state.

    import Combine
    import UIKit
    
    final class SearchViewModel {
    
        let query = PassthroughSubject<String, Never>()
    
        @Published private(set) var results: [String] = []
    
        private var cancellables = Set<AnyCancellable>()
    
        init() {
            query
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
                .removeDuplicates()
                .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
                .filter { !$0.isEmpty }
                .flatMap { q in
                    Self.searchPublisher(query: q)
                        .catch { _ in Just([]) }
                }
                .assign(to: &$results)
        }
    
        private static func searchPublisher(query: String) -> AnyPublisher<[String], Error> {
            Future { promise in
                DispatchQueue.global().asyncAfter(deadline: .now() + 0.2) {
                    promise(.success(["\(query) 1", "\(query) 2", "\(query) 3"]))
                }
            }
            .eraseToAnyPublisher()
        }
    }

    The main drawback is cognitive overhead and complexity. Combine introduces advanced abstractions (Publishers, Subscribers, Subjects, operator chains) that can be harder to debug and understand compared to imperative callbacks or simple target–action patterns. Error propagation and type signatures can become verbose, and misuse of operators (e.g., forgetting to manage cancellation or incorrectly handling threading) may introduce subtle bugs. For small or straightforward search implementations, a manual debounce using DispatchWorkItem or Timer can be simpler and easier to maintain, particularly for teams not already comfortable with reactive programming paradigms.

    final class SearchVC: UIViewController {
        private let vm = SearchViewModel()
        private var cancellables = Set<AnyCancellable>()
        private let textField = UITextField()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            textField.addTarget(self, action: #selector(textChanged), for: .editingChanged)
    
            vm.$results
                .receive(on: DispatchQueue.main)
                .sink { results in
                    print("Resultados:", results)
                }
                .store(in: &cancellables)
        }
    
        @objc private func textChanged() {
            vm.query.send(textField.text ?? "")
        }
    }

    Conclusions

    With the following examples, I did not intend to directly replace traditional development patterns with Combine, but rather to identify development scenarios where the Combine framework is an appropriate and valuable option.

    You can find the source code for this example in the following GitHub repository.

    References

    • Combine

      Apple Developer Documentation

  • iOS Dev between Combine and Delegate/Closure

    iOS Dev between Combine and Delegate/Closure

    When to use Combine versus the delegate and closure approaches is a great point to clarify because many developers struggle to choose the right tool for different scenarios, especially as Apple continues to expand its reactive frameworks while traditional patterns remain widely used. This post pretends to highlight the trade-offs—such as Combine’s power in managing complex, asynchronous data streams and chaining operators, versus the simplicity and low-overhead of closures for one-off callbacks or delegates for structured, reusable communication.

    The dilemma

    Combine, delegates, and closures all solve the same core problem: communicating results or events between components asynchronously.

    Here’s what they share in common:

    • Asynchronous communication → all three let one part of your app notify another when “something happened” without blocking.

    • Decoupling → the caller doesn’t need to know how the callee works, only what result or event it sends back.

    • Custom API design → you can choose the style (reactive stream, callback, or protocol) that best fits your use case, but under the hood they’re all just passing messages across boundaries.

    • Memory management concerns → all require care with retain cycles ([weak self] in closures, weak delegates, cancellables in Combine).

    • Interoperability → you can wrap one into another (e.g., turn a delegate or closure into a Publisher, or call a closure inside a delegate method).

    In short: they’re different abstractions over the same idea of event handling and result delivery, just with different levels of structure and power.

    The rule of thumb

    Use Combine when…

    • You’re dealing with streams of values over time (state updates, network retries, text search with debounce, multi-source merges).

    • You need operator chains (map/filter/flatMap/throttle/retry/catch) and cancellable pipelines you can pause or tear down with a single AnyCancellable.

    • You’re deep in SwiftUI (@Published, ObservableObject, view models) or want fan-out to multiple subscribers.

    • Testing with schedulers and time (virtual clocks) matters.

    Use delegates/closures when…

    • It’s a one-shot or simple callback (“user picked photo”, “request finished”, “button tapped”).

    • You want minimal overhead & maximal clarity in local APIs; fewer allocations and less indirection.

    • The interaction is structured and two-way (classic UIKit delegate lifecycles) or you’re writing a library that shouldn’t force a reactive dependency.

    Examples

    Example (type-ahead search with debouncing & cancellation):

    import Combine
    import UIKit
    
    final class SearchViewModel {
        @Published var query: String = ""
        @Published private(set) var results: [Repo] = []
    
        private var bag = Set<AnyCancellable>()
        private let api = GitHubAPI()
    
        init() {
            $query
                .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
                .removeDuplicates()
                .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
                .filter { !$0.isEmpty }
                .flatMap { q in
                    self.api.searchRepos(query: q)              // -> AnyPublisher<[Repo], Error>
                        .replaceError(with: [])                 // surface errors as empty for UX
                }
                .receive(on: RunLoop.main)
                .assign(to: &$results)
        }
    }
    
    struct Repo: Decodable { let name: String }
    
    final class GitHubAPI {
        func searchRepos(query: String) -> AnyPublisher<[Repo], Error> {
            let url = URL(string: "https://api.github.com/search/repositories?q=\(query)")!
            return URLSession.shared.dataTaskPublisher(for: url)
                .map(\.data)
                .decode(type: SearchResponse.self, decoder: JSONDecoder())
                .map(\.items)
                .eraseToAnyPublisher()
        }
    }
    
    struct SearchResponse: Decodable { let items: [Repo] }
    

    Example (delegate for a child picker reporting a single selection):

    protocol ColorPickerViewControllerDelegate: AnyObject {
        func colorPicker(_ picker: ColorPickerViewController, didSelect color: UIColor)
    }
    
    final class ColorPickerViewController: UIViewController {
        weak var delegate: ColorPickerViewControllerDelegate?
    
        // call once when a color is chosen
        private func didChoose(_ color: UIColor) {
            delegate?.colorPicker(self, didSelect: color)
            dismiss(animated: true)
        }
    }
    
    // Parent sets itself as delegate
    final class SettingsViewController: UIViewController, ColorPickerViewControllerDelegate {
        func presentPicker() {
            let picker = ColorPickerViewController()
            picker.delegate = self
            present(picker, animated: true)
        }
    
        func colorPicker(_ picker: ColorPickerViewController, didSelect color: UIColor) {
            view.backgroundColor = color
        }
    }
    

    Closure variant (simple one-shot completion):

    final class ImageLoader {
        func load(_ url: URL, completion: @escaping (Result<UIImage, Error>) -> Void) {
            URLSession.shared.dataTask(with: url) { data, _, error in
                if let e = error { completion(.failure(e)); return }
                guard let data = data, let image = UIImage(data: data) else {
                    completion(.failure(NSError(domain: "decode", code: -1)))
                    return
                }
                completion(.success(image))
            }.resume()
        }
    }
    

    Quick decision checklist

    • Do I expect multiple values over time that I want to transform/combine? → Combine

    • Is it a single, directed callback (child → parent or one network response)? → Delegate/Closure

    • Do I need easy cancellation, debouncing, retries, or operator chains? → Combine

    • Is simplicity, minimal dependencies, or pre–iOS 13 support important? → Delegate/Closure

    💡Tip: don’t force reactive patterns everywhere—use Combine where its composability shines, and keep the rest lightweight.

    Bridge between them

    Combine and delegates/closures can absolutely be mixed in the same project, and in fact that’s often the most pragmatic approach. They aren’t mutually exclusive, they just solve different layers of the problem:

    • Frameworks & SDKs: Many Apple and third-party APIs still expose delegates or completion handlers (e.g. CLLocationManager, URLSession).

    • Your app architecture: You might want to stay “Combine-first” in your view models, but still need to talk to APIs that rely on delegate or closure callbacks.

    Example: turning a delegate into a Combine publisher:
    import Combine
    import CoreLocation
    
    final class LocationManagerDelegateProxy: NSObject, CLLocationManagerDelegate {
        let subject = PassthroughSubject<CLLocation, Never>()
    
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            if let loc = locations.last {
                subject.send(loc)
            }
        }
    }
    
    // Usage
    let locationManager = CLLocationManager()
    let proxy = LocationManagerDelegateProxy()
    locationManager.delegate = proxy
    
    let cancellable = proxy.subject
        .sink { location in
            print("Got location:", location)
        }
    

    Example: wrapping a closure in a publisher

    func loadImagePublisher(from url: URL) -> AnyPublisher<UIImage, Error> {
        Future { promise in
            URLSession.shared.dataTask(with: url) { data, _, error in
                if let e = error {
                    promise(.failure(e))
                } else if let data, let image = UIImage(data: data) {
                    promise(.success(image))
                } else {
                    promise(.failure(NSError(domain: "decode", code: -1)))
                }
            }.resume()
        }
        .eraseToAnyPublisher()
    }
    

    Conclusions

    I hope this helped clarify when it makes sense to use Combine and when a delegate or closure is the better fit. Most importantly, remember that these approaches are not mutually exclusive—you don’t need to force reactive patterns into every situation. Instead, use Combine where its strengths truly shine, and rely on delegates or closures when simplicity is all you need.

    References

    • Combine

      Apple Developer Documentation

  • Dealing a REST API with Combine

    Dealing a REST API with Combine

    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