Async/Await vs. Combine: Real-World Use Cases and Best Practices
As iOS developers, you often face decisions on how to handle asynchronous tasks. With Swift's Async/Await and Combine, knowing which tool to use can make a significant difference. This article dives into practical app scenarios to help you choose the right tool for your specific challenges.
Overview:
Async/Await:
Combine:
Case 1: Fetching a User Profile in a Social Media App
When navigating to a user profile page, fetching the profile data needs to be efficient and easy to maintain. Using Async/Await can be beneficial here due to its simplicity and readability.
Benefits of Async/Await:
import SwiftUI
struct UserProfile: Decodable, Identifiable {
let id: UUID
let name: String
let bio: String
}
@MainActor
class UserProfileViewModel: ObservableObject {
@Published var profile: UserProfile?
@Published var errorMessage: String?
func fetchUserProfile() async {
do {
guard let url = URL(string: "https://api.socialmedia.com/user/profile") else {
throw URLError(.badURL)
}
let (data, _) = try await URLSession.shared.data(from: url)
let profile = try JSONDecoder().decode(UserProfile.self, from: data)
self.profile = profile
} catch {
self.errorMessage = error.localizedDescription
}
}
}
struct UserProfileView: View {
@StateObject private var viewModel = UserProfileViewModel()
var body: some View {
VStack {
if let profile = viewModel.profile {
Text(profile.name)
.font(.title)
Text(profile.bio)
.font(.subheadline)
} else if let errorMessage = viewModel.errorMessage {
Text(errorMessage)
.foregroundColor(.red)
} else {
Text("Loading...")
}
}
.onAppear {
Task {
await viewModel.fetchUserProfile()
}
}
}
}
Why Not Combine:
Case 2: Live Stock Price Updates in a Finance App
Displaying live stock price updates continuously in a finance app can benefit from Combine's capabilities.
Benefits of Combine:
import SwiftUI
import Combine
struct StockPrice: Decodable {
let price: Double
}
@MainActor
class StockViewModel: ObservableObject {
@Published var stockPrice: Double = 0.0
private var cancellables = Set<AnyCancellable>()
func startFetchingStockPrice() {
guard let url = URL(string: "https://api.stocks.com/price") else {
return
}
Timer.publish(every: 1.0, on: .main, in: .common)
.autoconnect()
.flatMap { _ in
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: StockPrice.self, decoder: JSONDecoder())
.catch { _ in Just(StockPrice(price: 0.0)) }
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.map { $0.price }
.assign(to: &$stockPrice)
}
}
struct StockPriceView: View {
@StateObject private var viewModel = StockViewModel()
var body: some View {
VStack {
Text("Stock Price: \(viewModel.stockPrice)")
.padding()
}
.onAppear {
viewModel.startFetchingStockPrice()
}
}
}
Why Not Async/Await:
Case 3: Search Autocomplete in an E-commerce App
Providing search suggestions as the user types can be optimized using Combine.
Benefits of Combine:
领英推荐
import Combine
import SwiftUI
@MainActor
class SearchViewModel: ObservableObject {
@Published var searchTerm: String = ""
@Published var suggestions: [String] = []
private var cancellables = Set<AnyCancellable>()
init() {
$searchTerm
.debounce(for: .milliseconds(300), scheduler: RunLoop.main) // Waits for 300ms of inactivity
.removeDuplicates() // Ensures the same input doesn't trigger a new request
.flatMap { query in
guard let url = URL(string: "https://api.ecommerce.com/search/suggestions?q=\(query)") else {
return Just([]).eraseToAnyPublisher()
}
return URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [String].self, decoder: JSONDecoder())
.replaceError(with: [])
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.assign(to: &$suggestions)
}
}
struct SearchView: View {
@StateObject private var viewModel = SearchViewModel()
var body: some View {
VStack {
TextField("Search", text: $viewModel.searchTerm)
.padding()
List(viewModel.suggestions, id: \.self) { suggestion in
Text(suggestion)
}
}
.padding()
}
}
Why Not Async/Await:
Case 4: Fetching Notifications in a Messaging App
Fetching user notifications and updating the UI can be more efficient with Combine.
Benefits of Combine:
import UIKit
import Combine
struct Notification: Identifiable, Decodable {
let id: UUID
let message: String
}
class NotificationsViewController: UITableViewController {
private var notifications: [Notification] = []
private var cancellables = Set<AnyCancellable>()
private let viewModel = NotificationsViewModel()
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "NotificationCell")
bindViewModel()
viewModel.fetchNotifications()
}
private func bindViewModel() {
viewModel.$notifications
.receive(on: DispatchQueue.main)
.sink { [weak self] notifications in
self?.notifications = notifications
self?.tableView.reloadData()
}
.store(in: &cancellables)
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return notifications.count
}
override func tableView(_ tableView: cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationCell", for: indexPath)
cell.textLabel?.text = notifications[indexPath.row].message
return cell
}
}
@MainActor
class NotificationsViewModel: ObservableObject {
@Published var notifications: [Notification] = []
private var cancellables = Set<AnyCancellable>()
func fetchNotifications() {
guard let url = URL(string: "https://api.messagingapp.com/notifications") else {
return
}
URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: [Notification].self, decoder: JSONDecoder())
.replaceError(with: [])
.assign(to: &$notifications)
}
}
Why Not Async/Await:
Case 5: Aggregating Weather Data in a Weather App
Fetching user notifications and updating the UI can be more efficient with Combine.
Benefits of Combine:
import Combine
import SwiftUI
struct Weather: Decodable {
let temperature: Double
static let empty = Weather(temperature: 0.0)
}
struct Forecast: Decodable {
let day: String
let temperature: Double
static let empty = Forecast(day: "", temperature: 0.0)
}
struct AirQuality: Decodable {
let index: Int
static let empty = AirQuality(index: 0)
}
@MainActor
class WeatherViewModel: ObservableObject {
@Published var currentWeather: Weather = Weather.empty
@Published var forecast: [Forecast] = []
@Published var airQuality: AirQuality = AirQuality.empty
private var cancellables = Set<AnyCancellable>()
func fetchWeatherData() {
guard let weatherURL = URL(string: "https://api.weather.com/current"),
let forecastURL = URL(string: "https://api.weather.com/forecast"),
let airQualityURL = URL(string: "https://api.weather.com/airquality") else {
return
}
let weatherPublisher = URLSession.shared.dataTaskPublisher(for: weatherURL)
.map(\.data)
.decode(type: Weather.self, decoder: JSONDecoder())
.catch { _ in Just(Weather.empty) }
.eraseToAnyPublisher()
let forecastPublisher = URLSession.shared.dataTaskPublisher(for: forecastURL)
.map(\.data)
.decode(type: [Forecast].self, decoder: JSONDecoder())
.catch { _ in Just([]) }
.eraseToAnyPublisher()
let airQualityPublisher = URLSession.shared.dataTaskPublisher(for: airQualityURL)
.map(\.data)
.decode(type: AirQuality.self, decoder: JSONDecoder())
.catch { _ in Just(AirQuality.empty) }
.eraseToAnyPublisher()
Publishers.CombineLatest3(weatherPublisher, forecastPublisher, airQualityPublisher)
.receive(on: DispatchQueue.main)
.sink { [weak self] weather, forecast, airQuality in
self?.currentWeather = weather
self?.forecast = forecast
self?.airQuality = airQuality
}
.store(in: &cancellables)
}
}
struct WeatherView: View {
@StateObject private var viewModel = WeatherViewModel()
var body: some View {
VStack {
Text("Current Temperature: \(viewModel.currentWeather.temperature)")
List(viewModel.forecast, id: \.day) { forecast in
Text("\(forecast.day): \(forecast.temperature)")
}
Text("Air Quality Index: \(viewModel.airQuality.index)")
}
.onAppear {
viewModel.fetchWeatherData()
}
}
}
Why Not Async/Await:
Choosing between Async/Await and Combine depends on the specific requirements of your application. Use Async/Await for straightforward asynchronous tasks where simplicity and readability are crucial. Opt for Combine when dealing with complex data streams, multiple API calls, or when you need advanced reactive programming features.
By understanding the strengths and weaknesses of Async/Await and Combine, and knowing when to use each, you can write more efficient and maintainable asynchronous code in Swift.
#iOS #iOSDeveloper #iOSEngineer #Swift #SwiftLang #MobileDevelopment #AppDevelopment #SoftwareEngineering #SwiftUI #Concurrency