Overview
AirQualityService is a Swift actor that provides methods to fetch current air quality data from the Open-Meteo Air Quality API. It supports fetching data for single locations and batch fetching for multiple cities.
Source: BreezeApp/Services/AirQualityService.swift:4
Actor Isolation
This service is implemented as an actor to ensure thread-safe access to its methods and properties. All async methods are actor-isolated and can be safely called from any context.
Shared Instance
static let shared = AirQualityService()
A singleton instance of the service that can be accessed throughout the application.
Methods
fetchAirQuality
Fetch current air quality data for a specific location.
func fetchAirQuality(
latitude: Double,
longitude: Double
) async throws -> AirQuality
The latitude of the location to fetch air quality data for
The longitude of the location to fetch air quality data for
Returns: AirQuality - An object containing current air quality metrics including:
- US AQI (Air Quality Index)
- PM10 and PM2.5 particulate matter
- Carbon monoxide levels
- Nitrogen dioxide levels
- Sulphur dioxide levels
- Ozone levels
Throws:
URLError.badURL - If the URL cannot be constructed
URLError.badServerResponse - If the server returns a non-200 status code
NSError (domain: “AirQualityService”, code: 1) - If no air quality data is available in the response
DecodingError - If the response cannot be decoded
Implementation Details:
The method constructs a URL with the following query parameters:
latitude and longitude for the location
current parameter requesting multiple pollutant measurements
timezone set to “auto” for automatic timezone detection
// Usage example
let service = AirQualityService.shared
do {
let airQuality = try await service.fetchAirQuality(
latitude: 37.7749,
longitude: -122.4194
)
print("AQI: \(airQuality.usAqi)")
print("PM2.5: \(airQuality.pm2_5)")
} catch {
print("Failed to fetch air quality: \(error)")
}
fetchMultipleCities
Fetch AQI data for multiple cities simultaneously, useful for ticker displays.
func fetchMultipleCities(
_ cities: [TopCity]
) async -> [(city: TopCity, aqi: Int?)]
An array of TopCity objects containing city information with latitude and longitude coordinates
Returns: [(city: TopCity, aqi: Int?)] - An array of tuples containing:
city: The original TopCity object
aqi: The US AQI value for that city, or nil if data couldn’t be fetched
Error Handling:
This method does not throw errors. Instead, it returns nil AQI values for cities where data couldn’t be fetched. Errors are logged to the console.
Implementation Details:
The method uses batch fetching by sending comma-separated latitude and longitude values in a single API request. This is more efficient than making individual requests for each city.
// API request format
let lats = cities.map { String($0.lat) }.joined(separator: ",")
let lons = cities.map { String($0.lon) }.joined(separator: ",")
The response is parsed to handle both array and single object responses from the API.
// Usage example
let cities = [
TopCity(name: "San Francisco", lat: 37.7749, lon: -122.4194),
TopCity(name: "Los Angeles", lat: 34.0522, lon: -118.2437),
TopCity(name: "New York", lat: 40.7128, lon: -74.0060)
]
let service = AirQualityService.shared
let results = await service.fetchMultipleCities(cities)
for (city, aqi) in results {
if let aqi = aqi {
print("\(city.name): AQI \(aqi)")
} else {
print("\(city.name): No data available")
}
}
API Endpoint
The service uses the Open-Meteo Air Quality API:
https://air-quality-api.open-meteo.com/v1/air-quality
Source Code
import Foundation
/// Service for fetching air quality data from Open-Meteo API
actor AirQualityService {
static let shared = AirQualityService()
private let baseURL = "https://air-quality-api.open-meteo.com/v1/air-quality"
/// Fetch current air quality for a location
func fetchAirQuality(latitude: Double, longitude: Double) async throws -> AirQuality {
var components = URLComponents(string: baseURL)!
components.queryItems = [
URLQueryItem(name: "latitude", value: String(latitude)),
URLQueryItem(name: "longitude", value: String(longitude)),
URLQueryItem(name: "current", value: "us_aqi,pm10,pm2_5,carbon_monoxide,nitrogen_dioxide,sulphur_dioxide,ozone"),
URLQueryItem(name: "timezone", value: "auto")
]
guard let url = components.url else {
throw URLError(.badURL)
}
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 else {
throw URLError(.badServerResponse)
}
let decoder = JSONDecoder()
let result = try decoder.decode(AirQualityResponse.self, from: data)
guard let airQuality = result.current else {
throw NSError(domain: "AirQualityService", code: 1,
userInfo: [NSLocalizedDescriptionKey: "No air quality data available"])
}
return airQuality
}
/// Fetch AQI for multiple cities (for ticker)
func fetchMultipleCities(_ cities: [TopCity]) async -> [(city: TopCity, aqi: Int?)] {
let lats = cities.map { String($0.lat) }.joined(separator: ",")
let lons = cities.map { String($0.lon) }.joined(separator: ",")
var components = URLComponents(string: baseURL)!
components.queryItems = [
URLQueryItem(name: "latitude", value: lats),
URLQueryItem(name: "longitude", value: lons),
URLQueryItem(name: "current", value: "us_aqi")
]
guard let url = components.url else {
return cities.map { ($0, nil) }
}
do {
let (data, _) = try await URLSession.shared.data(from: url)
// Parse the response - can be array or single object
if let json = try JSONSerialization.jsonObject(with: data) as? [[String: Any]] {
return zip(cities, json).map { city, data in
let current = data["current"] as? [String: Any]
let aqi = current?["us_aqi"] as? Int
return (city, aqi)
}
} else if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let current = json["current"] as? [String: Any],
let aqi = current["us_aqi"] as? Int,
let firstCity = cities.first {
return [(firstCity, aqi)]
}
} catch {
print("Error fetching multiple cities: \(error)")
}
return cities.map { ($0, nil) }
}
}
See Also