У меня есть пользовательские аннотации к карте, которые будут отображать метки, когда они расположены ближе к центру экрана. Я не знаю, как рассчитать физическое положение аннотаций на карте. Сделать это с помощью представления прокрутки и чтения геометрии довольно просто, но как это можно сделать с помощью представления карты?
Я пытался использовать это решение 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()
})
@Sweeper Нет, мне не нужна поддержка более ранних версий. Я пытаюсь использовать новые API. На самом деле у меня есть еще один вопрос по этому поводу: stackoverflow.com/q/78704924/18070009
вы можете создать класс пользовательских аннотаций, как показано ниже
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?
Вам следует перейти на новые 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)
}
}
}
Удивительный!!! Отличный ответ.
Быстрый вопрос. Когда я пытаюсь сразу получить доступ к vm.mapCameraPosition.region?.center, не касаясь экрана, он не равен нулю. Но когда я перемещаю карту и пытаюсь снова получить к ней доступ, она оказывается равной нулю. Знаете ли вы, почему это происходит? Буду признателен за помощь.
@AhmedZaidan Да, это ожидаемое поведение. Привязка $vm.mapCameraPosition
, которую вы передали Map(position: ...)
, полезна только для настройки камеры карты. Если вы хотите прочитать положение камеры на карте, вам следует сделать это в onMapCameraChange
, как я сделал.
@AhmedZaidan Точнее, когда вы перемещаете карту, vm.mapCameraPosition.positionedByUser
станет правдой, и вы не сможете получить из нее много полезной информации.
Спасибо. Немного похоже, но я хочу иметь такие функции, как карты Snapchat, куда добавляются метки пользователей, если между аннотациями достаточно места. Также аннотации не должны располагаться выше друг друга, и в этом случае их следует группировать вместе. Есть ли конкретная стратегия, которую я мог бы использовать для этого? Или мне просто нужно рассчитать расстояния между всеми метками?
@AhmedZaidan MapKit уже должен автоматически обрабатывать это (предотвращать перекрытие надписей), если только вы не используете собственные функции надписей MapKit и не создали свои собственные. Я бы предложил опубликовать еще один вопрос и показать минимальный воспроизводимый пример того, что вы делаете.
Похоже, вы все еще используете представление карты до iOS 17. Нужна ли вам поддержка более ранних версий? В iOS 17+ появились новые API просмотра карт, которые вам следует использовать. Кажется, вам просто нужен MapReader.