Replacing combine to async/await

With the advent of async/await in Swift 5.5, many things have been replaced. Typical examples are Closure and Combine, which helped asynchronous programming.

Async/await

If you look at the Apple's official document, the following tips are written. It is said to eliminate the need for closures and combines with async-await.

// You don’t need closure-based asychronicity patterns if you’re using the async-await features in Swift 5.5 and later.
// Instead, your code can await an asynchronous call, and then execute the code that would have been in the closure. 
// This eliminates the need for both conventional completion handlers and Combine futures.
// For more information, see Concurrency in The Swift Programming Language.

Example

There are three example files API.swift, Service.swift, and ViewModel.swift implemented in Combine as follows.

// API.swift
class API {
  func perform<T: Decodable>(method: HTTPMethod, url: String?) -> AnyPublisher<HTTPResponse<T>, Error> {
    ...
    
    return URLSession.shared.dataTaskPublisher(for: request)
      .tryMap { result -> HTTPResponse<T> in
        let value = try self.decoder.decode(T.self, from: result.data)
        return HTTPResponse(value: value, response: result.response)
      }
      .receive(on: DispatchQueue.main)
      .eraseToAnyPublisher()
  }
}

// Service.swift
class Service: ServiceType {    
  ...
  
  func request() -> AnyPublisher<[SomeType], Error> {
    ...
    
    return API.shared.perform(method: .get, url: component?.string)
      .map(\.value)
      .eraseToAnyPublisher()
  }
}

// ViewModel.swift
func requestSomething() {
  self.service.request()
    .sink { _ in
      ...
    } receiveValue: { response in
      self.response = response
    }.store(in: &self.cancellable)
}

API.swift

First, I removed AnyPublisher from the function declaration and added async throws.

// Combine
func perform<T: Decodable>(method: HTTPMethod, url: String?) -> AnyPublisher<HTTPResponse<T>, Error>

// Async/await
func perform<T: Decodable>(method: HTTPMethod, url: String?) async throws -> HTTPResponse<T>

Then change the return type accordingly. Use the async method data(from:) instead of the dataTaskPublisher.

// Combine
return URLSession.shared.dataTaskPublisher(for: request)
  .tryMap { result -> HTTPResponse<T> in
      let value = try self.decoder.decode(T.self, from: result.data)
      return HTTPResponse(value: value, response: result.response)
  }
  .receive(on: DispatchQueue.main)
  .eraseToAnyPublisher()    
    
// Async/await
let (data, urlResponse) = try await URLSession.shared.data(from: url)
let response = try decoder.decode(T.self, from: data)
return HTTPResponse(value: response, response: urlResponse)

Service.swift

This is the change in Service.swift. Similarly, I removed AnyPublisher and added async throws.

// Combine
func request() -> AnyPublisher<[SomeType], Error> {}

// Async/await
func request() async throws -> [SomeType]

As expected, I will modify the return accordingly.

// Combine
return API.shared.perform(method: .get, url: component?.string)
  .map(\.value)
  .eraseToAnyPublisher()
    
// Async/await
return try await API.shared.perform(method: .get, url: component?.string).value

ViewModel.swift

For ViewModel file, I received the value in Task instead of using sink.

// Combine
func requestSomething() {
  self.service.request()
    .sink { _ in
      ...
    } receiveValue: { response in
      self.response = response
    }.store(in: &self.cancellable)
}

// Async/await
func requestSomething()  {
  Task {
    self.response = try await self.service.request()
  }
}

You can call this as the following way.

View()
  .onAppear { 
    self.viewModel.requestSomething()
  }

Result

These are the results of three files implemented using async/await instead of Combine. Personally, I think it's much more intuitive and simple.

// API.swift
class API {
  func perform<T: Decodable>(method: HTTPMethod, url: String?) async throws -> HTTPResponse<T> {
    ...
    
    let (data, urlResponse) = try await URLSession.shared.data(from: url)
    let response = try decoder.decode(T.self, from: data)
    return HTTPResponse(value: response, response: urlResponse)
  }
}

// Service.swift
class Service: ServiceType {    
  ...
  
  func request() async throws -> [SomeType]
    ...
        
    return try await API.shared.perform(method: .get, url: component?.string).value
  }
}

// ViewModel.swift
func requestSomething()  {
  Task {
    self.response = try await self.service.request()
  }
}