Search code examples
swiftswiftuimapkit

SwiftUI .popover on tap of Map Marker


I'm trying to get a .popover to work with a Map > Marker but can't seem to get it. I've tried to go about this by getting the coordinates and presenting a .popover based on that, but I can't get it to work. Simply putting the .popover modifier on Image("Logo") didn't seem to work either. Here is my current code

import SwiftUI
import MapKit

struct MapView: View {
    @State private var region: MKCoordinateRegion
    var libraries: [Library]
    @State private var selectedLibrary: Library?

    init(libraries: [Library]) {
        if let firstLibrary = libraries.first {
            _region = State(initialValue: MKCoordinateRegion(
                center: firstLibrary.coordinate,
                span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
            ))
        } else {
            _region = State(initialValue: MKCoordinateRegion())
        }
        self.libraries = libraries
    }

    var body: some View {
        ZStack {
            Map {
                ForEach(libraries) { library in
                    Marker(coordinate: library.coordinate) {
                        Image("Logo")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 30, height: 30)
                    }
                    .tint(.purple)
                }
            }
            .edgesIgnoringSafeArea(.all)

            GeometryReader { geometry in
                ForEach(libraries) { library in
                    Button(action: {
                        self.selectedLibrary = library
                    }, label: {
                        Color.clear
                    })
                    .frame(width: 44, height: 44)
                    .position(self.coordinateToPoint(library.coordinate, in: geometry))
                    .popover(isPresented: Binding<Bool>(
                        get: { self.selectedLibrary == library },
                        set: { if !$0 { self.selectedLibrary = nil } }
                    )) {
                        Text("Annotation details here")
                            .padding()
                    }
                }
            }
        }
    }

    private func coordinateToPoint(_ coordinate: CLLocationCoordinate2D, in geometry: GeometryProxy) -> CGPoint {
        let mapWidthDegrees = region.span.longitudeDelta
        let mapHeightDegrees = region.span.latitudeDelta

        let widthPerDegree = geometry.size.width / mapWidthDegrees
        let heightPerDegree = geometry.size.height / mapHeightDegrees

        let xCoordinate = (coordinate.longitude - region.center.longitude + mapWidthDegrees / 2) * widthPerDegree
        let yCoordinate = (region.center.latitude - coordinate.latitude + mapHeightDegrees / 2) * heightPerDegree

        return CGPoint(x: xCoordinate, y: yCoordinate)
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(libraries: LibraryData.libraries)
    }
}

And here is my LibraryData and Library for reference

import CoreLocation

struct Library: Identifiable, Equatable {
    let id = UUID()
    let coordinate: CLLocationCoordinate2D

    static func == (lhs: Library, rhs: Library) -> Bool {
        lhs.id == rhs.id &&
        lhs.coordinate.latitude == rhs.coordinate.latitude &&
        lhs.coordinate.longitude == rhs.coordinate.longitude
    }
}

struct LibraryData {
    static let libraries = [
        Library(coordinate: CLLocationCoordinate2D(latitude: 37.33182, longitude: -122.03118)),
        Library(coordinate: CLLocationCoordinate2D(latitude: 37.34182, longitude: -122.03218)),
        Library(coordinate: CLLocationCoordinate2D(latitude: 37.34182, longitude: -122.04118))
    ]
}

When I tap the marker inside the map nothing happens. Any help would be appreciated. Thanks in advance.


Solution

  • You could try a different approach using Annotation instead of Marker and the .popover().

    The example code shows how to tap on any Annotation and popup a custom view.

    struct ContentView: View {
        var body: some View {
            MapView(libraries: LibraryData.libraries)
        }
    }
    
    struct MapView: View {
        @State private var region: MKCoordinateRegion
        var libraries: [Library]
        @State private var selectedLibrary: Library?
        
        // for testing
        @State private var cameraPosition: MapCameraPosition = .camera(
            MapCamera(centerCoordinate: CLLocationCoordinate2D(latitude: 37.33182, longitude: -122.03118), distance: 8000.0, heading: 0, pitch: 0)
        )
        
        init(libraries: [Library]) {
            if let firstLibrary = libraries.first {
                _region = State(initialValue: MKCoordinateRegion(
                    center: firstLibrary.coordinate,
                    span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
                ))
            } else {
                _region = State(initialValue: MKCoordinateRegion())
            }
            self.libraries = libraries
        }
        
        @State private var toggler = false  // <-- here to toggle the popup view
        
        var body: some View {
            Map(position: $cameraPosition) {
                ForEach(libraries) { library in
                    Annotation("", coordinate: library.coordinate) {
                        ZStack {
                            Image(systemName: "mappin.circle.fill")
                                .resizable()
                                .scaledToFit()
                                .frame(width: 30, height: 30)
                                .onTapGesture {
                                    selectedLibrary = library
                                    toggler.toggle()
                                }
                                .foregroundStyle(.white, .purple)
                            if selectedLibrary == library && toggler {
                                PopupView(library: selectedLibrary)
                            } else {
                                EmptyView()
                            }
                        }
                    }
                }
            }
            .edgesIgnoringSafeArea(.all)
        }
    }
    
    // just for testing, adjust the looks as needed
     struct PopupView: View {
        @State var library: Library?
        
        var body: some View {
            VStack {
                if let lib = library {
                    Text("Poping: \(String(lib.id.uuidString.prefix(4)))")
                } else {
                    Text("no data")
                }
            }
            .foregroundStyle(.purple)
            .frame(width: 120, height: 100)
            .background(Color.white)
            .cornerRadius(10)
            .shadow(radius: 5)
            .offset(x: 0, y: -70)
        }
    }
    

    EDIT-1:

    you can use a .popover(...) if this is really what you want. I feel it does not looks great on ios devices, but looks good on mac/macCatalyst and iPad, and the Button works.

    struct MapView: View {
        @State private var region: MKCoordinateRegion
        var libraries: [Library]
        
        // for testing
        @State private var cameraPosition: MapCameraPosition = .camera(
            MapCamera(centerCoordinate: CLLocationCoordinate2D(latitude: 37.33182, longitude: -122.03118), distance: 8000.0, heading: 0, pitch: 0)
        )
        
        init(libraries: [Library]) {
            if let firstLibrary = libraries.first {
                _region = State(initialValue: MKCoordinateRegion(
                    center: firstLibrary.coordinate,
                    span: MKCoordinateSpan(latitudeDelta: 0.02, longitudeDelta: 0.02)
                ))
            } else {
                _region = State(initialValue: MKCoordinateRegion())
            }
            self.libraries = libraries
        }
        
        var body: some View {
            Map(position: $cameraPosition) {
                ForEach(libraries) { library in
                    Annotation("", coordinate: library.coordinate) {
                        MakerView(library: library)
                    }
                }
            }
            .edgesIgnoringSafeArea(.all)
        }
        
    }
    
    struct MakerView: View {
        @State var library: Library
        @State private var showPopover = false
        
        var body: some View {
                Image(systemName: "mappin.circle.fill")
                    .resizable()
                    .scaledToFit()
                    .frame(width: 30, height: 30)
                    .onTapGesture {
                        showPopover = true
                    }
                    .foregroundStyle(.white, .purple)
    
            .popover(isPresented: $showPopover) {
                Text("Annotation details here")
                Button("click me"){
                    print("----> Button clicked")
                }.buttonStyle(.bordered)
            }
        }
    }