Search code examples
iosswiftui

Tapping SwiftUI Alert button fetches wrong data


In code below, tapping on Annotation displays an Alert with two buttons, "Cancel" and "Buy".

Tapping the Annotation or the Alert "Buy" button also prints some information to console, but the "Buy" button doesn't always print the correct information.

For example, tapping the Annotation will print "Toyota Camry 2001". Tapping Alert "Buy" button sometimes prints "Ford Mustang 2000" instead, sometimes prints "Toyota Camry 2001".

Same behaviour in 17.5 and 18. Trying to understand why or if it's a bug?

import SwiftUI
import MapKit

struct Car: Hashable {
    let make: String
    let model: String
    let year: Int
    let lat: Double
    let lng: Double
}

struct ContentView: View {
    
    let cars: [Car] = [
        .init(make: "Ford", model: "Mustang", year: 2000, lat: 40.7117, lng: -74.0049),
        .init(make: "Toyota", model: "Camry", year: 2001, lat: 40.7127, lng: -74.0059)
    ]
    
    @State private var position = MapCameraPosition.automatic
    @State private var showAlert = false
    
    var body: some View {
        
        Map(position: $position) {
            ForEach(cars, id: \.self) { car in
                Annotation(coordinate: CLLocationCoordinate2D(latitude: car.lat, longitude: car.lng)) {
                    Image(systemName: "mappin.circle.fill")
                        .font(.system(size: 50))
                        .onTapGesture {
                            showAlert.toggle()
                            print(car.make, car.model, car.year)
                        }
                        .alert(
                            "Question",
                            isPresented: $showAlert
                        ) {
                            HStack {
                                Button("Cancel", role: .cancel) {
                                    // do nothing
                                }
                                Button("Buy") {
                                    print(car.make, car.model, car.year)
                                }
                            }
                        } message: {
                            Text("Do you want to buy this car?")
                        }
                    
                }
                label: {
                    Text(car.model)
                }
            }
        }
        .mapStyle(.standard(elevation: .automatic))
    }
}

#Preview {
    ContentView()
}

Solution

  • Try this approach using struct Car: Identifiable,... with a ForEach(cars){...} and moving the .alert outside the ForEach loop using the support of selectedCar, as shown in the example code:

    Note, the ForEach loop must have unique elements, here supplied by the Identifiable, do not use ForEach(cars, id: \.self). Also the .alert should not be in the ForEach loop. With those changes the code will show the correct car info.

    struct Car: Identifiable, Hashable {   // <--- here
        let id = UUID()  // <--- here
        
        let make: String
        let model: String
        let year: Int
        let lat: Double
        let lng: Double
    }
    
    struct ContentView: View {
        
        let cars: [Car] = [
            .init(make: "Ford", model: "Mustang", year: 2000, lat: 40.7117, lng: -74.0049),
            .init(make: "Toyota", model: "Camry", year: 2001, lat: 40.7127, lng: -74.0059)
        ]
        
        @State private var position = MapCameraPosition.automatic
        @State private var showAlert = false
        
        @State private var selectedCar: Car?  // <--- here
        
        var body: some View {
            
            Map(position: $position) {
                ForEach(cars) { car in  // <--- here
                    Annotation(coordinate: CLLocationCoordinate2D(latitude: car.lat, longitude: car.lng)) {
                        Image(systemName: "mappin.circle.fill")
                            .font(.system(size: 50))
                            .onTapGesture {
                                selectedCar = car  // <--- here
                                print(car.make, car.model, car.year)
                                showAlert.toggle()
                            }
                    }
                    label: {
                        Text(car.model)
                    }
                }
            }
            .mapStyle(.standard(elevation: .automatic))
            // --- here
            .alert(
                "Question",
                isPresented: $showAlert,
                presenting: selectedCar
            ) { car in
                HStack {
                    Button("Cancel", role: .cancel) {
                        // do nothing
                    }
                    Button("Buy") {
                        print("--> buy ", car.make, car.model, car.year)
                    }
                }
            } message: { _ in
                Text("Do you want to buy this car?")
            }
            
        }
    }