Skip to main content
Breeze uses the MVVM (Model-View-ViewModel) architecture pattern. The DashboardViewModel serves as the central state manager for the app.

DashboardViewModel

Main view model implementing ObservableObject for SwiftUI data binding.
BreezeApp/ViewModels/DashboardViewModel.swift
@MainActor
class DashboardViewModel: NSObject, ObservableObject {
    // Published properties
    @Published var airQuality: AirQuality?
    @Published var pollutants: [PollutantReading] = []
    @Published var pollenItems: [PollenItem] = []
    @Published var climateData: [ClimateDataPoint] = []
    @Published var locationName: String = ""
    @Published var isLoading = false
    @Published var errorMessage: String?
    @Published var searchQuery = ""
    @Published var searchResults: [City] = []
    @Published var useFahrenheit = true
    
    private let locationManager = CLLocationManager()
    private var searchTask: Task<Void, Never>?
}

Published Properties

Data Properties

airQuality
AirQuality?
Current air quality data including AQI and pollutant concentrations. nil when no data is loaded.
pollutants
[PollutantReading]
Array of individual pollutant readings (PM2.5, PM10, NO₂, SO₂, O₃, CO) with status calculations.
pollenItems
[PollenItem]
Pollen data including general types (Grass, Tree, Weed) and specific plants.
climateData
[ClimateDataPoint]
Historical temperature data for climate trend visualization.

UI State Properties

locationName
String
Display name for current location (e.g., “San Francisco, USA” or “Your Location”)
isLoading
Bool
Loading state indicator for UI feedback
errorMessage
String?
Error message to display to user if data fetch fails

Search Properties

searchQuery
String
Current search input text
searchResults
[City]
Array of city search results from geocoding API

Settings Properties

useFahrenheit
Bool
Temperature unit preference (true = Fahrenheit, false = Celsius)

Computed Properties

aqiStatus

Computes human-readable status from current AQI value.
BreezeApp/ViewModels/DashboardViewModel.swift
var aqiStatus: AQIStatus? {
    guard let aqi = airQuality?.usAQI else { return nil }
    return AQIStatus.from(aqi: aqi)
}
aqiStatus
AQIStatus?
Status object containing text, emoji, description, color, and health tips

aqiColor

Maps AQI status color name to SwiftUI Color.
BreezeApp/ViewModels/DashboardViewModel.swift
var aqiColor: Color {
    guard let status = aqiStatus else { return .gray }
    switch status.color {
    case "aqiGood": return .aqiGood
    case "aqiModerate": return .aqiModerate
    case "aqiUnhealthySensitive": return .aqiUnhealthySensitive
    case "aqiUnhealthy": return .aqiUnhealthy
    case "aqiVeryUnhealthy": return .aqiVeryUnhealthy
    case "aqiHazardous": return .aqiHazardous
    default: return .gray
    }
}

Location Methods

requestLocation

Requests user’s current location with authorization handling.
BreezeApp/ViewModels/DashboardViewModel.swift
func requestLocation() {
    isLoading = true
    errorMessage = nil
    
    switch locationManager.authorizationStatus {
    case .notDetermined:
        locationManager.requestWhenInUseAuthorization()
    case .authorizedWhenInUse, .authorizedAlways:
        locationManager.requestLocation()
    case .denied, .restricted:
        errorMessage = "Location access denied. Please enable in Settings."
        isLoading = false
    @unknown default:
        errorMessage = "Unknown location authorization status."
        isLoading = false
    }
}
This method handles all authorization states and provides user-friendly error messages for denied access.

CLLocationManagerDelegate

Implements location delegate methods for receiving location updates.
BreezeApp/ViewModels/DashboardViewModel.swift
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.first else { return }
    
    locationName = "Your Location"
    
    // Reverse geocode for display name
    CLGeocoder().reverseGeocodeLocation(location) { [weak self] placemarks, _ in
        if let placemark = placemarks?.first {
            var components: [String] = []
            if let city = placemark.locality {
                components.append(city)
            }
            if let country = placemark.country {
                components.append(country)
            }
            if !components.isEmpty {
                self?.locationName = components.joined(separator: ", ")
            }
        }
    }
    
    Task {
        await fetchAllData(
            latitude: location.coordinate.latitude,
            longitude: location.coordinate.longitude
        )
    }
}

func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
    errorMessage = "Unable to get your location."
    isLoading = false
}

Search Methods

searchCities

Searches for cities with debouncing to reduce API calls.
BreezeApp/ViewModels/DashboardViewModel.swift
func searchCities(_ query: String) {
    searchTask?.cancel()
    
    guard query.count >= 2 else {
        searchResults = []
        return
    }
    
    searchTask = Task {
        do {
            try await Task.sleep(nanoseconds: 300_000_000) // 300ms debounce
            let results = try await GeocodingService.shared.searchCities(query: query)
            if !Task.isCancelled {
                self.searchResults = results
            }
        } catch {
            if !Task.isCancelled {
                print("Search error: \(error)")
            }
        }
    }
}
query
String
required
Search term (minimum 2 characters)
The method implements 300ms debouncing to avoid making API calls on every keystroke, improving performance and reducing server load.

selectCity

Handles city selection from search results.
BreezeApp/ViewModels/DashboardViewModel.swift
func selectCity(_ city: City) {
    locationName = city.displayName
    searchResults = []
    searchQuery = ""
    
    Task {
        await fetchAllData(latitude: city.latitude, longitude: city.longitude)
    }
}
city
City
required
Selected city from search results

Data Fetching Methods

fetchAllData

Fetches all environmental data for a location (air quality, pollen, climate).
BreezeApp/ViewModels/DashboardViewModel.swift
func fetchAllData(latitude: Double, longitude: Double) async {
    isLoading = true
    errorMessage = nil
    
    // Fetch air quality (blocking)
    do {
        let aq = try await AirQualityService.shared.fetchAirQuality(
            latitude: latitude,
            longitude: longitude
        )
        self.airQuality = aq
        
        // Create pollutant readings
        self.pollutants = [
            PollutantReading(type: .pm25, value: aq.pm25),
            PollutantReading(type: .pm10, value: aq.pm10),
            PollutantReading(type: .no2, value: aq.nitrogenDioxide),
            PollutantReading(type: .so2, value: aq.sulphurDioxide),
            PollutantReading(type: .o3, value: aq.ozone),
            PollutantReading(type: .co, value: aq.carbonMonoxide)
        ]
    } catch {
        self.errorMessage = "Unable to fetch air quality data."
        print("Air quality error: \(error)")
    }
    
    // Fetch pollen data (non-blocking)
    Task {
        do {
            let pollen = try await PollenService.shared.fetchPollen(
                latitude: latitude,
                longitude: longitude
            )
            self.pollenItems = pollen
        } catch {
            print("Pollen error: \(error)")
        }
    }
    
    // Fetch climate data (non-blocking)
    Task {
        do {
            let climate = try await ClimateService.shared.fetchClimateData(
                latitude: latitude,
                longitude: longitude
            )
            self.climateData = climate
        } catch {
            print("Climate error: \(error)")
        }
    }
    
    isLoading = false
}
latitude
Double
required
Latitude coordinate
longitude
Double
required
Longitude coordinate
Air quality data is fetched first (blocking) since it’s essential for the main UI. Pollen and climate data are fetched in parallel non-blocking tasks to improve perceived performance.

Temperature Formatting Methods

formatTemperature

Formats temperature based on user’s unit preference.
BreezeApp/ViewModels/DashboardViewModel.swift
func formatTemperature(_ celsius: Double) -> String {
    if useFahrenheit {
        let fahrenheit = (celsius * 9 / 5) + 32
        return String(format: "%.1f°F", fahrenheit)
    }
    return String(format: "%.1f°C", celsius)
}
celsius
Double
required
Temperature in Celsius
return
String
Formatted temperature string (e.g., “72.0°F” or “22.2°C”)

formatTemperatureDiff

Formats temperature difference with sign indicator.
BreezeApp/ViewModels/DashboardViewModel.swift
func formatTemperatureDiff(_ celsiusDiff: Double) -> String {
    let value = useFahrenheit ? celsiusDiff * 9 / 5 : celsiusDiff
    let unit = useFahrenheit ? "°F" : "°C"
    let sign = value >= 0 ? "+" : ""
    return String(format: "%@%.1f%@", sign, value, unit)
}
celsiusDiff
Double
required
Temperature difference in Celsius
return
String
Formatted difference with sign (e.g., “+3.6°F” or “-1.2°C”)

Usage Example

struct ContentView: View {
    @StateObject private var viewModel = DashboardViewModel()
    
    var body: some View {
        VStack {
            if let aqi = viewModel.airQuality?.usAQI {
                Text("AQI: \(aqi)")
                    .foregroundColor(viewModel.aqiColor)
                
                if let status = viewModel.aqiStatus {
                    Text("\(status.emoji) \(status.text)")
                    Text(status.description)
                }
            }
            
            Button("Use My Location") {
                viewModel.requestLocation()
            }
        }
        .onAppear {
            viewModel.requestLocation()
        }
    }
}

Thread Safety

The @MainActor attribute ensures all view model operations run on the main thread:
  • Safe UI updates from published properties
  • No data races on shared state
  • Automatic thread synchronization
@MainActor
class DashboardViewModel: NSObject, ObservableObject {
    // All methods and properties guaranteed to run on main thread
}

Build docs developers (and LLMs) love