Breeze uses SwiftUI for its declarative UI framework. The view hierarchy is organized around a main ContentView that orchestrates navigation and state management.
ContentView
Root view of the application managing app-wide state and navigation.
BreezeApp/App/ContentView.swift
struct ContentView: View {
@StateObject private var viewModel = DashboardViewModel()
@State private var showSearch = false
@State private var showSettings = false
@AppStorage("appearanceMode") private var appearanceMode: AppearanceMode = .system
var body: some View {
NavigationStack {
ZStack {
Color.appBackground
.ignoresSafeArea()
if viewModel.isLoading && viewModel.airQuality == nil {
LoadingView()
} else if let airQuality = viewModel.airQuality {
DashboardView(viewModel: viewModel)
} else {
// Landing view with search and location buttons
}
}
.toolbar { /* ... */ }
.sheet(isPresented: $showSearch) {
SearchView(viewModel: viewModel)
}
.sheet(isPresented: $showSettings) {
SettingsView()
}
}
.preferredColorScheme(appearanceMode.colorScheme)
}
}
State Management
Main state object for air quality and environmental data
Controls search sheet presentation
Controls settings sheet presentation
Persisted appearance preference (system/light/dark) using @AppStorage
View States
ContentView renders different UI based on data availability:
- Loading State - Shows
LoadingView spinner
- Dashboard State - Shows
DashboardView with data
- Landing State - Shows welcome screen with search and location options
Landing View
Initial state when no location data is loaded:
BreezeApp/App/ContentView.swift
VStack(spacing: 24) {
Spacer()
// Logo and text
HStack(spacing: 12) {
Image(systemName: "wind")
.font(.system(size: 40, weight: .light))
.foregroundStyle(.linearGradient(
colors: [.blue, .cyan],
startPoint: .topLeading,
endPoint: .bottomTrailing
))
Text("Breeze")
.font(.system(size: 36, weight: .semibold, design: .rounded))
}
AnimatedText(text: "Take a deep breath")
.font(.title3)
.foregroundColor(.secondary)
Spacer()
// Search button
Button {
showSearch = true
} label: {
HStack {
Image(systemName: "magnifyingglass")
Text("Search for a city")
Spacer()
}
.padding()
.background(Color.searchBarBackground)
.clipShape(Capsule())
}
// Location button
Button {
viewModel.requestLocation()
} label: {
Label("Use My Location", systemImage: "location.circle.fill")
}
}
Dynamic toolbar shown when data is loaded:
BreezeApp/App/ContentView.swift
.toolbar {
if viewModel.airQuality != nil {
ToolbarItem(placement: .navigationBarLeading) {
Button {
// Clear data to return to landing page
viewModel.airQuality = nil
viewModel.pollutants = []
viewModel.pollenItems = []
viewModel.climateData = []
viewModel.locationName = ""
} label: {
Image(systemName: "house")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: 16) {
Button { showSearch = true } label: {
Image(systemName: "magnifyingglass")
}
Button { showSettings = true } label: {
Image(systemName: "gearshape")
}
}
}
}
}
DashboardView
Main dashboard displaying all environmental data.
BreezeApp/Views/Dashboard/DashboardView.swift
struct DashboardView: View {
@ObservedObject var viewModel: DashboardViewModel
var body: some View {
ScrollView {
VStack(spacing: 20) {
// Location Header
VStack(spacing: 4) {
Text(viewModel.locationName)
.font(.title)
.fontWeight(.semibold)
Text(Date().formatted(date: .complete, time: .omitted))
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.top)
// AQI Card
AQICard(viewModel: viewModel)
// Pollen Section
if !viewModel.pollenItems.isEmpty {
PollenView(items: viewModel.pollenItems)
}
// Pollutants Grid
PollutantsGrid(pollutants: viewModel.pollutants)
// Climate Chart
if !viewModel.climateData.isEmpty {
ClimateChartView(
data: viewModel.climateData,
useFahrenheit: $viewModel.useFahrenheit,
formatTemp: viewModel.formatTemperature,
formatDiff: viewModel.formatTemperatureDiff
)
}
Spacer(minLength: 40)
}
.padding(.horizontal)
}
.refreshable {
// Pull to refresh functionality
}
}
}
Components
Displays city name and current date
Main air quality index card with status and health tips
Pollen data visualization (conditionally shown)
Grid of individual pollutant readings
Historical temperature trend chart (conditionally shown)
Pull to Refresh
The refreshable modifier enables pull-to-refresh gesture:
.refreshable {
if let coords = getCurrentCoordinates() {
await viewModel.fetchAllData(latitude: coords.lat, longitude: coords.lon)
}
}
Dashboard Components
AQICard
Displays AQI value with color-coded status and health information.
BreezeApp/Views/Dashboard/AQICard.swift
struct AQICard: View {
@ObservedObject var viewModel: DashboardViewModel
var body: some View {
VStack(spacing: 16) {
// AQI number with emoji
if let aqi = viewModel.airQuality?.usAQI,
let status = viewModel.aqiStatus {
Text("\(status.emoji)")
.font(.system(size: 60))
Text("\(aqi)")
.font(.system(size: 72, weight: .bold))
.foregroundColor(viewModel.aqiColor)
Text(status.text)
.font(.title2)
.fontWeight(.semibold)
Text(status.description)
.font(.body)
.multilineTextAlignment(.center)
.foregroundColor(.secondary)
// Health tips
HealthTipsView(tips: status.tips)
}
}
.padding()
.background(Color.cardBackground)
.cornerRadius(20)
}
}
PollutantsGrid
Grid layout showing individual pollutant concentrations.
BreezeApp/Views/Dashboard/PollutantsGrid.swift
struct PollutantsGrid: View {
let pollutants: [PollutantReading]
var body: some View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
ForEach(pollutants) { pollutant in
PollutantCard(pollutant: pollutant)
}
}
}
}
struct PollutantCard: View {
let pollutant: PollutantReading
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: pollutant.type.icon)
.foregroundColor(pollutant.status.color)
Text(pollutant.type.rawValue)
.font(.headline)
}
Text("\(pollutant.roundedValue) \(pollutant.type.unit)")
.font(.title3)
.fontWeight(.semibold)
Text(pollutant.status.rawValue)
.font(.caption)
.foregroundColor(pollutant.status.color)
}
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.cardBackground)
.cornerRadius(12)
}
}
PollenView
Horizontal scrolling list of pollen items.
BreezeApp/Views/Environmental/PollenView.swift
struct PollenView: View {
let items: [PollenItem]
var body: some View {
VStack(alignment: .leading) {
Text("Pollen Forecast")
.font(.title2)
.fontWeight(.semibold)
.padding(.horizontal)
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(items) { item in
PollenCard(item: item)
}
}
.padding(.horizontal)
}
}
}
}
ClimateChartView
Line chart showing historical temperature trends.
BreezeApp/Views/Environmental/ClimateChartView.swift
struct ClimateChartView: View {
let data: [ClimateDataPoint]
@Binding var useFahrenheit: Bool
let formatTemp: (Double) -> String
let formatDiff: (Double) -> String
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Climate Trends")
.font(.title2)
.fontWeight(.semibold)
// Chart implementation using Charts framework
Chart(data) { point in
LineMark(
x: .value("Year", point.year),
y: .value("Temperature", point.temperature)
)
.foregroundStyle(.blue)
PointMark(
x: .value("Year", point.year),
y: .value("Temperature", point.temperature)
)
.foregroundStyle(.blue)
}
.frame(height: 200)
}
.padding()
.background(Color.cardBackground)
.cornerRadius(20)
}
}
Search View
City search interface with real-time results.
BreezeApp/Views/Search/SearchView.swift
struct SearchView: View {
@ObservedObject var viewModel: DashboardViewModel
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
VStack {
// Search bar
TextField("Search for a city", text: $viewModel.searchQuery)
.textFieldStyle(.roundedBorder)
.padding()
.onChange(of: viewModel.searchQuery) { _, newValue in
viewModel.searchCities(newValue)
}
// Results list
List(viewModel.searchResults) { city in
Button {
viewModel.selectCity(city)
dismiss()
} label: {
VStack(alignment: .leading) {
Text(city.name)
.font(.headline)
Text(city.displayName)
.font(.caption)
.foregroundColor(.secondary)
}
}
}
}
.navigationTitle("Search")
.navigationBarTitleDisplayMode(.inline)
}
}
}
Utility Components
LoadingView
Simple loading indicator.
BreezeApp/Views/Components/LoadingView.swift
struct LoadingView: View {
var body: some View {
VStack(spacing: 20) {
ProgressView()
.scaleEffect(1.5)
Text("Loading...")
.foregroundColor(.secondary)
}
}
}
AnimatedText
Text with fade-in animation.
BreezeApp/Views/Components/AnimatedText.swift
struct AnimatedText: View {
let text: String
@State private var opacity: Double = 0
var body: some View {
Text(text)
.opacity(opacity)
.onAppear {
withAnimation(.easeIn(duration: 1.0)) {
opacity = 1.0
}
}
}
}
HealthTipsView
Scrollable list of health tips.
BreezeApp/Views/Components/HealthTipsView.swift
struct HealthTipsView: View {
let tips: [String]
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Health Tips")
.font(.headline)
ForEach(tips, id: \.self) { tip in
HStack(alignment: .top) {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(tip)
.font(.body)
}
}
}
.padding()
.background(Color.tipBackground)
.cornerRadius(12)
}
}
Settings View
App settings and preferences.
BreezeApp/Views/Components/SettingsView.swift
struct SettingsView: View {
@AppStorage("appearanceMode") private var appearanceMode: AppearanceMode = .system
@AppStorage("useFahrenheit") private var useFahrenheit: Bool = true
@Environment(\.dismiss) var dismiss
var body: some View {
NavigationStack {
Form {
Section("Appearance") {
Picker("Theme", selection: $appearanceMode) {
ForEach(AppearanceMode.allCases) { mode in
Text(mode.displayName).tag(mode)
}
}
}
Section("Units") {
Toggle("Use Fahrenheit", isOn: $useFahrenheit)
}
}
.navigationTitle("Settings")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") { dismiss() }
}
}
}
}
}
Theme Colors
Custom colors defined in Color+Theme.swift:
.appBackground - Main app background
.cardBackground - Card and component backgrounds
.searchBarBackground - Search bar background
.aqiGood - Good AQI level (green)
.aqiModerate - Moderate AQI level (yellow)
.aqiUnhealthySensitive - Unhealthy for sensitive groups (orange)
.aqiUnhealthy - Unhealthy level (red)
.aqiVeryUnhealthy - Very unhealthy (purple)
.aqiHazardous - Hazardous level (maroon)
View Modifiers
Common view modifiers used throughout:
.preferredColorScheme() - Apply appearance mode
.refreshable - Pull to refresh
.sheet() - Present modal sheets
.toolbar() - Configure navigation bar
.padding() - Add spacing
.background() - Apply background colors
.cornerRadius() - Round corners
.font() - Typography