Skip to main content

Overview

The SearchView component provides a full-screen modal interface for searching and selecting locations. It features a search bar with real-time city lookup, a “Use Current Location” button, search results list, and a curated list of popular cities.

Visual Display

The search view presents:
  • Navigation bar with “Search” title and Cancel button
  • Rounded search bar with magnifying glass icon
  • Clear button (X) when text is entered
  • “Use Current Location” button with location icon
  • Live search results as user types
  • Popular cities list when search is empty
  • “No results found” state

Component Definition

struct SearchView: View {
    @ObservedObject var viewModel: DashboardViewModel
    @Environment(\.dismiss) private var dismiss
    @FocusState private var isSearchFocused: Bool
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                // Search bar
                HStack {
                    Image(systemName: "magnifyingglass")
                        .foregroundColor(.secondary)
                    
                    TextField("Search for a city", text: $viewModel.searchQuery)
                        .focused($isSearchFocused)
                        .textFieldStyle(.plain)
                        .autocorrectionDisabled()
                        .onChange(of: viewModel.searchQuery) { _, newValue in
                            viewModel.searchCities(newValue)
                        }
                    
                    if !viewModel.searchQuery.isEmpty {
                        Button {
                            viewModel.searchQuery = ""
                            viewModel.searchResults = []
                        } label: {
                            Image(systemName: "xmark.circle.fill")
                                .foregroundColor(.secondary)
                        }
                    }
                }
                .padding(12)
                .background(Color.searchBarBackground)
                .clipShape(Capsule())
                .padding()
            }
            .navigationTitle("Search")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
        }
        .onAppear {
            isSearchFocused = true
        }
    }
}

Props / Parameters

viewModel
DashboardViewModel
required
Observable view model that handles:
  • searchQuery: Current search text (bound to TextField)
  • searchResults: Array of cities matching the search
  • searchCities(): Method to perform city lookup
  • selectCity(): Method to select a city from results
  • requestLocation(): Method to request user’s current location
  • locationName: Current location name

Key Features

Search Bar with Auto-focus

Search bar automatically focuses when view appears:
@FocusState private var isSearchFocused: Bool

HStack {
    Image(systemName: "magnifyingglass")
        .foregroundColor(.secondary)
    
    TextField("Search for a city", text: $viewModel.searchQuery)
        .focused($isSearchFocused)
        .textFieldStyle(.plain)
        .autocorrectionDisabled()
        .onChange(of: viewModel.searchQuery) { _, newValue in
            viewModel.searchCities(newValue)
        }
    
    if !viewModel.searchQuery.isEmpty {
        Button {
            viewModel.searchQuery = ""
            viewModel.searchResults = []
        } label: {
            Image(systemName: "xmark.circle.fill")
                .foregroundColor(.secondary)
        }
    }
}

.onAppear {
    isSearchFocused = true
}

Current Location Button

Prominent button to use device location:
Button {
    dismiss()
    viewModel.requestLocation()
} label: {
    HStack {
        Image(systemName: "location.fill")
            .foregroundColor(.accentColor)
        
        Text("Use Current Location")
            .fontWeight(.medium)
        
        Spacer()
        
        Image(systemName: "chevron.right")
            .font(.caption)
            .foregroundColor(.secondary)
    }
    .padding()
    .background(Color.cardBackground)
    .clipShape(RoundedRectangle(cornerRadius: 12))
}
.buttonStyle(.plain)
.padding(.horizontal)

Live Search Results

Displays matching cities as user types (minimum 2 characters):
if !viewModel.searchResults.isEmpty {
    List(viewModel.searchResults) { city in
        Button {
            viewModel.selectCity(city)
            dismiss()
        } label: {
            VStack(alignment: .leading, spacing: 4) {
                Text(city.name)
                    .font(.headline)
                
                Text(city.displayName)
                    .font(.subheadline)
                    .foregroundColor(.secondary)
            }
            .padding(.vertical, 4)
        }
        .buttonStyle(.plain)
    }
    .listStyle(.plain)
}
Displays curated list when search is empty:
VStack(alignment: .leading, spacing: 12) {
    Text("Popular Cities")
        .font(.headline)
        .padding(.horizontal)
        .padding(.top, 24)
    
    ScrollView {
        LazyVStack(spacing: 0) {
            ForEach(TopCity.all.prefix(10), id: \.name) { city in
                Button {
                    viewModel.locationName = "\(city.name), \(city.country)"
                    dismiss()
                    Task {
                        await viewModel.fetchAllData(latitude: city.lat, longitude: city.lon)
                    }
                } label: {
                    HStack {
                        VStack(alignment: .leading) {
                            Text(city.name)
                                .font(.body)
                            Text(city.country)
                                .font(.caption)
                                .foregroundColor(.secondary)
                        }
                        Spacer()
                        Image(systemName: "chevron.right")
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                    .padding()
                }
                .buttonStyle(.plain)
                
                Divider()
                    .padding(.leading)
            }
        }
    }
}

Empty State

Shows when search has no results:
else if viewModel.searchQuery.count >= 2 {
    VStack {
        Spacer()
        Text("No results found")
            .foregroundColor(.secondary)
        Spacer()
    }
}

iOS Version Compatibility

The component includes version-specific code for iOS 17’s new onChange API:
if #available(iOS 17.0, *) {
    TextField("Search for a city", text: $viewModel.searchQuery)
        .focused($isSearchFocused)
        .textFieldStyle(.plain)
        .autocorrectionDisabled()
        .onChange(of: viewModel.searchQuery) { _, newValue in
            viewModel.searchCities(newValue)
        }
} else {
    // Fallback on earlier versions
}

User Flow

  1. User taps search icon on dashboard
  2. SearchView presents as modal sheet
  3. Search bar auto-focuses for immediate typing
  4. User can:
    • Type city name to search (triggers at 2+ characters)
    • Tap “Use Current Location” to use GPS
    • Select from popular cities list
  5. Selecting any option dismisses the view and updates dashboard
Modal presentation with toolbar:
.navigationTitle("Search")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
    ToolbarItem(placement: .navigationBarTrailing) {
        Button("Cancel") {
            dismiss()
        }
    }
}

Usage in App

Presented as a sheet from the dashboard:
.sheet(isPresented: $showingSearch) {
    SearchView(viewModel: dashboardViewModel)
}

Search Behavior

  • Real-time search as user types
  • Minimum 2 characters to trigger search
  • Search query debounced in view model
  • Results include city name and full display name (city, state, country)
  • Clear button appears when text is entered
Default list includes major cities worldwide:
  • New York, London, Tokyo, Paris, Los Angeles
  • And 5+ more major metropolitan areas

Source Location

BreezeApp/Views/Search/SearchView.swift:3

Build docs developers (and LLMs) love