Получите аннотации к карте в центре экрана Swift UI

У меня есть пользовательские аннотации к карте, которые будут отображать метки, когда они расположены ближе к центру экрана. Я не знаю, как рассчитать физическое положение аннотаций на карте. Сделать это с помощью представления прокрутки и чтения геометрии довольно просто, но как это можно сделать с помощью представления карты?

Я пытался использовать это решение SO , но оно не сработало, так как мне пришлось использовать контроллер представления MKMapView. И мне не удалось найти способ использовать пользовательские аннотации с контроллером представления MKMapView. Кроме того, я хотел бы рассчитать масштаб или масштаб карты, чтобы изменить размер аннотаций. Любые указатели будут с благодарностью оценены.

Копировать код с возможностью вставки

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()
})

Похоже, вы все еще используете представление карты до iOS 17. Нужна ли вам поддержка более ранних версий? В iOS 17+ появились новые API просмотра карт, которые вам следует использовать. Кажется, вам просто нужен MapReader.

Sweeper 04.07.2024 14:31

@Sweeper Нет, мне не нужна поддержка более ранних версий. Я пытаюсь использовать новые API. На самом деле у меня есть еще один вопрос по этому поводу: stackoverflow.com/q/78704924/18070009

Ahmed Zaidan 04.07.2024 15:36
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
80
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

вы можете создать класс пользовательских аннотаций, как показано ниже

    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)
        }
    }
}

после этого вы можете применить это, как показано ниже, код

  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
        }

и вы можете увеличивать и уменьшать масштаб, как показано ниже.

 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
}

Спасибо. Это работает, но кажется сложным реализовать сложную аннотацию быстрого пользовательского интерфейса карты. Можно ли как-нибудь сделать это с помощью картридера, как предложил @sweeper?

Ahmed Zaidan 04.07.2024 22:01
Ответ принят как подходящий

Вам следует перейти на новые API Map, добавленные в iOS 17.

Сначала замените mapRegion на MapCameraPosition:

@Published var mapCameraPosition = MapCameraPosition.automatic

чтобы ты смог сделать Map(position: $vm.mapCameraPosition) { ... }

Вы можете установить это на MKCoordinateRegion вот так (в updateMapRegion):

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

Во-вторых, я бы добавил новое свойство в LocationMap, чтобы указать, следует ли отображать его метку.

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

Это новое свойство затем можно установить в 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
                }
            }
        }
    }
}

Обратите внимание, что я передаю location.shouldShowName ? location.name : "" в качестве метки аннотации. MapKit автоматически решит, где и когда показывать эту метку, в дополнение к вашей собственной логике. Если это нежелательно, создайте свой собственный ярлык, например. как наложение круга.

Полный код:

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)
        }
    }
}

Удивительный!!! Отличный ответ.

Ahmed Zaidan 05.07.2024 03:24

Быстрый вопрос. Когда я пытаюсь сразу получить доступ к vm.mapCameraPosition.region?.center, не касаясь экрана, он не равен нулю. Но когда я перемещаю карту и пытаюсь снова получить к ней доступ, она оказывается равной нулю. Знаете ли вы, почему это происходит? Буду признателен за помощь.

Ahmed Zaidan 05.07.2024 07:11

@AhmedZaidan Да, это ожидаемое поведение. Привязка $vm.mapCameraPosition, которую вы передали Map(position: ...), полезна только для настройки камеры карты. Если вы хотите прочитать положение камеры на карте, вам следует сделать это в onMapCameraChange, как я сделал.

Sweeper 05.07.2024 07:20

@AhmedZaidan Точнее, когда вы перемещаете карту, vm.mapCameraPosition.positionedByUser станет правдой, и вы не сможете получить из нее много полезной информации.

Sweeper 05.07.2024 07:23

Спасибо. Немного похоже, но я хочу иметь такие функции, как карты Snapchat, куда добавляются метки пользователей, если между аннотациями достаточно места. Также аннотации не должны располагаться выше друг друга, и в этом случае их следует группировать вместе. Есть ли конкретная стратегия, которую я мог бы использовать для этого? Или мне просто нужно рассчитать расстояния между всеми метками?

Ahmed Zaidan 05.07.2024 19:44

@AhmedZaidan MapKit уже должен автоматически обрабатывать это (предотвращать перекрытие надписей), если только вы не используете собственные функции надписей MapKit и не создали свои собственные. Я бы предложил опубликовать еще один вопрос и показать минимальный воспроизводимый пример того, что вы делаете.

Sweeper 06.07.2024 01:12

Другие вопросы по теме