Ahmed Zaidan
Ahmed Zaidan

Reputation: 68

Get Map Annotations in center of screen Swift UI

I have custom map annotations that will display a label when they are positioned near the center of the screen. I am not sure how to calculate the physical position of the map annotations on the map. Doing this with a scroll view and geometry reader is quite simple, but how can this be done with a Map View?

I tried to use this SO solution but it didn't work as I would need to use a MKMapView view controller. And I was not able to find a way to use custom annotations with a MKMapView view controller. Additionally I would like to calculate the zoom or scale of the map to modify the size of the annotations. Any pointers would be greatly appreciated.

Copy Paste-able code

import SwiftUI
import MapKit

struct LocationsView: View {
    @EnvironmentObject private var vm: LocationsViewModel
    @State var scale: CGFloat = 0.0
    
    var body: some View {
        ZStack {
            mapLayer.ignoresSafeArea()
        }
    }
}

// --- WARNINGS HERE --

extension LocationsView {
    private var mapLayer: some View {
        Map(coordinateRegion: $vm.mapRegion,
            annotationItems: vm.locations,
            annotationContent: { location in
            MapAnnotation(coordinate: location.coordinates) {
                //my custom map annotations
                Circle().foregroundStyle(.red).frame(width: 50, height: 50)
                    .onTapGesture {
                        vm.showNextLocation(location: location)
                    }
            }
        })
    }
}

class LocationsViewModel: ObservableObject {
    @Published var locations: [LocationMap]    // All loaded locations
    @Published var mapLocation: LocationMap {    // Current location on map
        didSet {
            updateMapRegion(location: mapLocation)
        }
    }
    @Published var mapRegion: MKCoordinateRegion = MKCoordinateRegion() // Current region on map
    let mapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    
    init() {
        let locations = LocationsDataService.locations
        self.locations = locations
        self.mapLocation = locations.first!
        
        self.updateMapRegion(location: locations.first!)
    }
    private func updateMapRegion(location: LocationMap) {
        withAnimation(.easeInOut) {
            mapRegion = MKCoordinateRegion(
                center: location.coordinates,
                span: mapSpan)
        }
    }
    func showNextLocation(location: LocationMap) {
        withAnimation(.easeInOut) {
            mapLocation = location
        }
    }
}

struct LocationMap: Identifiable {
    var id: String = UUID().uuidString
    let name: String
    let cityName: String
    let coordinates: CLLocationCoordinate2D
}

class LocationsDataService {
    static let locations: [LocationMap] = [
        LocationMap(name: "Colosseum", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.8902, longitude: 12.4922)),
        LocationMap(name: "Pantheon", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.8986, longitude: 12.4769)),
        LocationMap(name: "Trevi Fountain", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.9009, longitude: 12.4833))
    ]
}

struct SwiftfulMapAppApp: View {
    @StateObject private var vm = LocationsViewModel()
    var body: some View {
        VStack {
            LocationsView().environmentObject(vm)
        }
    }
}

#Preview(body: {
    SwiftfulMapAppApp()
})

Upvotes: 1

Views: 359

Answers (2)

Sweeper
Sweeper

Reputation: 270683

You should migrate to the new Map APIs added in iOS 17.

First, change mapRegion to a MapCameraPosition:

@Published var mapCameraPosition = MapCameraPosition.automatic

so that you can do Map(position: $vm.mapCameraPosition) { ... }

You can set this to a MKCoordinateRegion like this (in updateMapRegion):

withAnimation(.easeInOut) {
    mapCameraPosition = .region(MKCoordinateRegion(
        center: location.coordinates,
        span: mapSpan))
}

Second, I would add a new property in LocationMap to indicate whether its label should be shown.

struct LocationMap: Identifiable {
    let id: String = UUID().uuidString
    let name: String
    let cityName: String
    let coordinates: CLLocationCoordinate2D
    var shouldShowName = false // <---
}

This new property can then be set in onMapCameraChange:

private var mapLayer: some View {
    MapReader { mapProxy in
        Map(position: $vm.mapCameraPosition) {
            ForEach(vm.locations) { location in
                Annotation(
                    location.shouldShowName ? location.name : "",
                    coordinate: location.coordinates) {
                        Circle().foregroundStyle(.red).frame(width: 50, height: 50)
                            .onTapGesture {
                                vm.showNextLocation(location: location)
                            }
                    }
            }
        }
        .onMapCameraChange(frequency: .continuous) { context in
            guard let center = mapProxy.convert(context.region.center, to: .local) else { return }
            for i in vm.locations.indices {
                if let point = mapProxy.convert(vm.locations[i].coordinates, to: .local) {
                    // the label should be shown when the annotation is within 50 points from the centre of the map
                    vm.locations[i].shouldShowName = abs(point.x - center.x) < 50 && abs(point.y - center.y) < 50
                } else {
                    vm.locations[i].shouldShowName = false
                }
            }
        }
    }
}

Note that I am passing location.shouldShowName ? location.name : "" as the label of the annotation. MapKit will automatically decide where and when to show this label, in addition to your own logic. If this is undesirable, build your own label e.g. as an overlay of the circle.

Full code:

struct LocationsView: View {
    @EnvironmentObject private var vm: LocationsViewModel
    
    var body: some View {
        ZStack {
            mapLayer.ignoresSafeArea()
        }
    }
    
    private var mapLayer: some View {
        MapReader { mapProxy in
            Map(position: $vm.mapCameraPosition) {
                ForEach(vm.locations) { location in
                    Annotation(
                        location.shouldShowName ? location.name : "",
                        coordinate: location.coordinates) {
                            Circle().foregroundStyle(.red).frame(width: 50, height: 50)
                                .onTapGesture {
                                    vm.showNextLocation(location: location)
                                }
                        }
                }
            }
            .onMapCameraChange(frequency: .continuous) { context in
                guard let center = mapProxy.convert(context.region.center, to: .local) else { return }
                for i in vm.locations.indices {
                    if let point = mapProxy.convert(vm.locations[i].coordinates, to: .local) {
                        vm.locations[i].shouldShowName = abs(point.x - center.x) < 250 && abs(point.y - center.y) < 250
                    } else {
                        vm.locations[i].shouldShowName = false
                    }
                }
            }
        }
    }
}

class LocationsViewModel: ObservableObject {
    @Published var locations: [LocationMap]    // All loaded locations
    @Published var mapLocation: LocationMap {    // Current location on map
        didSet {
            updateMapRegion(location: mapLocation)
        }
    }
    @Published var mapCameraPosition = MapCameraPosition.automatic
    let mapSpan = MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1)
    
    init() {
        let locations = LocationsDataService.locations
        self.locations = locations
        self.mapLocation = locations.first!
        
        self.updateMapRegion(location: locations.first!)
    }
    private func updateMapRegion(location: LocationMap) {
        withAnimation(.easeInOut) {
            mapCameraPosition = .region(MKCoordinateRegion(
                center: location.coordinates,
                span: mapSpan))
        }
    }
    func showNextLocation(location: LocationMap) {
        withAnimation(.easeInOut) {
            mapLocation = location
        }
    }
}

struct LocationMap: Identifiable {
    let id: String = UUID().uuidString
    let name: String
    let cityName: String
    let coordinates: CLLocationCoordinate2D
    var shouldShowName = false
}

class LocationsDataService {
    static let locations: [LocationMap] = [
        LocationMap(name: "Colosseum", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.8902, longitude: 12.4922)),
        LocationMap(name: "Pantheon", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.8986, longitude: 12.4769)),
        LocationMap(name: "Trevi Fountain", cityName: "Rome", coordinates: CLLocationCoordinate2D(latitude: 41.9009, longitude: 12.4833))
    ]
}

struct SwiftfulMapAppApp: View {
    @StateObject private var vm = LocationsViewModel()
    var body: some View {
        VStack {
            LocationsView().environmentObject(vm)
        }
    }
}

Upvotes: 3

Jay Pratap singh
Jay Pratap singh

Reputation: 31

you can create class of custom annotation like below

    class CustomAnnotation: NSObject, MKAnnotation {
    var coordinate: CLLocationCoordinate2D
    
    var location: SearchLocation?
    var sizeOf : CGRect?
    var title: String?
    var subtitle: String?
    
    init(location: SearchLocation? , sizeOf: CGRect? , coordinate: CLLocationCoordinate2D) {
        self.location = location
        self.sizeOf = sizeOf
        self.coordinate = coordinate
        self.title = location?.title ?? ""
        self.subtitle = location?.full_address ?? ""
    }
}



class CustomAnnotationView: MKAnnotationView {    
    override var annotation: MKAnnotation? {
        willSet {
            guard let customAnnotation = newValue as? CustomAnnotation else { return }
            
            canShowCallout = false
            // Create a container view
            let containerView = UIView(frame: customAnnotation.sizeOf ?? CGRect(x: 0, y: 0, width: 40, height: 40))
            
            // Create the image view for the user photo
            let imageView = UIImageView(frame: CGRect(x: 8, y: 2, width: 24, height: 24))
            let url = URL(string: Constant.COMMON_IMG_URL + (customAnnotation.location?.vendor_image ?? "") )
            print(url)
            imageView.kf.setImage(with: url,placeholder: offerPlaceHolderSquare)
            imageView.contentMode = .scaleAspectFill
            imageView.layer.cornerRadius = 12
            imageView.clipsToBounds = true
            
            // Add the image view to the container view
            containerView.addSubview(imageView)
            
            // Create the pin background
            let pinImageView = UIImageView(frame: containerView.bounds)
            pinImageView.image = UIImage(named: "pin2") // Add your custom pin background image to your assets
            containerView.insertSubview(pinImageView, at: 0)
            
            // Set the container view as the annotation view
            addSubview(containerView)
            
            // Set the frame of the annotation view to fit the container view
            frame = containerView.frame
            centerOffset = CGPoint(x: 0, y: -frame.size.height / 2)
        }
    }
}

after that you can apply that like below code

  func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            guard let customAnnotation = annotation as? CustomAnnotation else { return nil }
            
            let identifier = "CustomAnnotationView"
            
            var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) as? CustomAnnotationView
            
            if annotationView == nil {
                annotationView = CustomAnnotationView(annotation: customAnnotation, reuseIdentifier: identifier)
            } else {
                annotationView?.annotation = customAnnotation
            }
            
            return annotationView
        }

and you can do zoomin and zoom out like below

 func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
    view.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}
func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
    view.transform = CGAffineTransform.identity
}

Upvotes: 1

Related Questions