Я работаю над MapKit для SwiftUI, используя iOS 17, где у меня есть собственные аннотации. Когда выбрана аннотация, должен отображаться DetailsView
, но он не работает должным образом.
DetailsView
, даже если аннотация отображается на карте как выбранная.Следуя Видео WWDC23: Знакомьтесь с MapKit для SwiftUI в 17:07, я создал специальную аннотацию, которая при выборе увеличивает свой размер, как и ожидалось. Четко указано, что к аннотации должен быть прикреплен tag(_:)
, чтобы выбор работал.
Но параметр Map(selection:)
, похоже, не обновляется правильно, чтобы вызвать DetailsView
с помощью модификатора sheet(item:_:)
.
Как я могу гарантировать, что DetailsView
сработает должным образом, а также отменит выбор аннотации при закрытии представления?
Это минималистичный код, который я извлек из своего проекта:
import MapKit
import SwiftUI
struct ContentView: View {
private var annotations = generateRandomLocations()
@State private var selection: AnnotationModel?
var body: some View {
Map(selection: $selection) {
ForEach(annotations) { annotation in
AnnotationMarker(annotation: annotation)
}
}
.sheet(item: $selection) { station in
DetailsView(id: station.id)
.presentationDetents([.medium])
}
}
}
struct DetailsView: View {
var id: UUID
var body: some View {
Text("DetailsView: \(id)")
}
}
#Preview {
ContentView()
}
struct AnnotationMarker: MapContent {
var annotation: AnnotationModel
@State private var isSelected = false
var body: some MapContent {
Annotation(coordinate: annotation.coordinate) {
CustomMarker(isSelected: $isSelected)
} label: {
Text(annotation.id.uuidString)
}
.tag(annotation)
.annotationTitles(isSelected ? .visible : .hidden)
}
}
struct CustomMarker: View {
@Binding var isSelected: Bool
var body: some View {
ZStack {
Circle()
.frame(width: isSelected ? 52 : 28, height: isSelected ? 52 : 28)
.foregroundStyle(.green)
Image(systemName: "house")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: isSelected ? 32 : 16)
.foregroundStyle(.white)
}
.onTapGesture { withAnimation { isSelected.toggle() }}
}
}
struct AnnotationModel: Identifiable, Hashable {
let id = UUID()
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
}
var latitude: Double
var longitude: Double
}
/// Create random location for testing purposes.
func generateRandomLocations(count: Int = 50) -> [AnnotationModel] {
return (1...count).map { _ in
let latitude = Double.random(in: (43.5673...43.6573))
let longitude = Double.random(in: (3.8176...3.9076))
return AnnotationModel(latitude: latitude, longitude: longitude)
}
}
Я нашел способ решить эту проблему. Прежде всего, похоже, это ошибка с выбором предметов Map
. Если у Annotation
есть label
, нажатие на label
вызовет выбор Annotation
, как и ожидалось.
Однако, поскольку заголовок не требуется, хитрость заключается в том, чтобы передать selection: AnnotationModel
как Binding
в CustomMarker
и оттуда управлять выбранным и невыбранным поведением.
Ниже приведен обновленный код с комментариями рядом с добавленными частями:
ContentView
там, где Map
struct ContentView: View {
private var annotations = generateRandomLocations()
@State private var selection: AnnotationModel?
var body: some View {
Map(selection: $selection) {
ForEach(annotations) { annotation in
AnnotationMarker(
annotation: annotation,
selection: $selection // Pass the `selection` in the `AnnotationMarker`.
)
}
}
.sheet(item: $selection) { item in
DetailsView(id: item.id)
.presentationDetents([.medium])
}
}
}
Обновленный Annotation
с кастомным View
struct AnnotationMarker: MapContent {
var annotation: AnnotationModel
@Binding var selection: AnnotationModel?
var body: some MapContent {
Annotation(coordinate: annotation.coordinate) {
CustomMarker(
annotation: annotation, // Pass the `annotation` from the `ForEach` to the `CustomMarker `.
selection: $selection) // Pass the `selection` to the `CustomMarker `.
} label: {
Text(String())
}
.tag(annotation) // The tag can now be set here.
.annotationTitles(.hidden)
}
}
И именно здесь будут обрабатываться действия selection
внутри CustomMarker
.
struct CustomMarker: View {
@State private var isSelected = false
var annotation: AnnotationModel // Pass the `annotation` of this `CustomMarker`.
@Binding var selection: AnnotationModel? // Defining which `annotation` is selected, if any.
var body: some View {
ZStack {
Circle()
.frame(width: isSelected ? 52 : 28, height: isSelected ? 52 : 28)
.foregroundStyle(.green)
Image(systemName: "house")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: isSelected ? 32 : 16)
.foregroundStyle(.white)
}
.onTapGesture { // If it is the selected `annotation` from the `ForEach`, define the selection.
selection = annotation
withAnimation(.bouncy) { isSelected = true }
}
.onChange(of: selection) { // If the previous selected `annotation` from the `ForEach` is unselected, perform the changes.
guard isSelected, $1 == nil else { return } // Avoid having actions on unselected `annotations`.
withAnimation(.bouncy) { isSelected = false }
}
}
}
вам, вероятно, понадобится вычисленная привязка для элемента листа, чтобы она не мешала выбору карты.