Reputation: 475
The thing is, I can't find any documentation on this--does anyone know if there is a way to neatly deal with annotations in the same spot (either so that you can like click the annotation or a button to cycle through the annotations at that spot or something else)? I just need a way to cycle through the annotations in a specific spot and access them individually. Any help and/or suggestions would be greatly appreciated.
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
//I tried to do a check here for if selectedAnnotation == annotation { somehow cycle to the next annotation in that location } but I guess when you click an already selectedAnnotation, the didDeselect function is run or something
selectedAnnotation = annotation
mapView.setCenter(annotation.coordinate, zoomLevel: 17, animated: true)
}
My annotation function looks like:
class AnnotationsVM: ObservableObject {
@Published var annos = [MGLPointAnnotation]()
@ObservedObject var VModel: ViewModel //= ViewModel()
init(VModel: ViewModel) {
self.VModel = VModel
let annotation = MGLPointAnnotation()
annotation.title = "Shoe Store"
annotation.coordinate = CLLocationCoordinate2D(latitude: 40.78, longitude: -73.98)
annotation.subtitle = "10:00AM - 11:30AM"
annos.append(annotation)
}
func addNextAnnotation(address: String) {
let newAnnotation = MGLPointAnnotation()
self.VModel.fetchCoords(address: address) { lat, lon in
if (lat != 0.0 && lon != 0.0) {
newAnnotation.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
newAnnotation.title = address
newAnnotation.subtitle = "9:00PM - 1:00AM"
if (lat != 0 && lon != 0) {
self.annos.append(newAnnotation)
}
}
}
}
In my updateUIView function in the MapView (UIViewRepresentable) struct, I add the annos array to the map.
Upvotes: 1
Views: 1261
Reputation: 475
I ended up doing my own implementation--I made an array of annotations with the same latitude and longitude and then I added buttons to the custom callout to cycle through that array, keeping track of the annotation that I am looking at.
Upvotes: 0
Reputation: 1508
Update to the previous answer:
I (as so often) misread the question. So here is an updated repo that does the following:
You can add as many locations to the same spot (spot A & spot B) as you want. When you select a spot a custom view opens and shows some more infos. You can then loop though all locations which are at the same spot. This is done by comparing the latitude and longitude from the initial selected spot.
I pushed everything to github
I try to keep it short: In the model i get all the locations from the same spot, count it and set it for the views.
import SwiftUI
import Combine
import Mapbox
struct AnnotationLocation{
let latitude: Double
let longitude: Double
let title: String?
}
/// Source of Truth
class AnnotationModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
var annotationsForOperations: [AnnotationLocation] = [AnnotationLocation]()
@Published var locationsAtSameSpot: [AnnotationLocation] = [AnnotationLocation]()
@Published var showCustomCallout: Bool = false
@Published var countSameSpots: Int = 0
@Published var selectedAnnotation: AnnotationLocation = AnnotationLocation(latitude: 0, longitude: 0, title: nil)
func addLocationInModel(annotation: MGLPointAnnotation) {
let newSpot = AnnotationLocation(latitude: annotation.coordinate.latitude, longitude: annotation.coordinate.longitude, title: annotation.title ?? "No Title")
annotationsForOperations.append(newSpot)
}
func getAllLocationsFormSameSpot() {
locationsAtSameSpot = [AnnotationLocation]()
for annotation in annotationsForOperations {
if annotation.latitude == selectedAnnotation.latitude &&
annotation.longitude == selectedAnnotation.longitude {
locationsAtSameSpot.append(annotation)
}
}
}
func getNextAnnotation(index: Int) -> Bool {
if locationsAtSameSpot.indices.contains(index + 1) {
selectedAnnotation = locationsAtSameSpot[index + 1]
return true
} else {
return false
}
}
}
The MapView delegate set the inital location and fires the function in the model to get all locations from the same spot.
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
/// Create a customLoaction and assign it the model
/// The values are needed to loop though the same annotations
let customAnnotation = AnnotationLocation(latitude: annotation.coordinate.latitude, longitude: annotation.coordinate.longitude, title: annotation.title ?? "No Tilte")
/// assignselected annotion @EnvironmentObject
/// so it can be shown in the custom callout
annotationModel.selectedAnnotation = customAnnotation
/// show custom call out
annotationModel.showCustomCallout = true
/// count locations at same spot
/// also pushes same locations into separte array to loop through
annotationModel.getAllLocationsFormSameSpot()
mapView.setCenter(annotation.coordinate, zoomLevel: 17, animated: true)
}
Finally the SwiftUI View, should be self explaining...
import SwiftUI
import Mapbox
struct ContentView: View {
@EnvironmentObject var annotationModel: AnnotationModel
@State var annotations: [MGLPointAnnotation] = [MGLPointAnnotation]()
@State private var showAnnotation: Bool = false
@State private var nextAnnotation: Int = 0
var body: some View {
GeometryReader{ g in
VStack{
ZStack(alignment: .top){
MapView(annotations: self.$annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16).environmentObject(self.annotationModel)
if self.annotationModel.showCustomCallout {
VStack{
HStack{
Spacer()
Button(action: {
self.annotationModel.showCustomCallout = false
}) {
Image(systemName: "xmark")
.foregroundColor(Color.black)
.font(Font.system(size: 12, weight: .regular))
}.offset(x: -5, y: 5)
}
HStack{
Text("Custom Callout")
.font(Font.system(size: 12, weight: .regular))
.foregroundColor(Color.black)
}
Spacer()
Text("Selected: \(self.annotationModel.selectedAnnotation.title ?? "No Tiltle")")
.font(Font.system(size: 16, weight: .regular))
.foregroundColor(Color.black)
Text("Count same Spot: \(self.annotationModel.locationsAtSameSpot.count) ")
.font(Font.system(size: 16, weight: .regular))
.foregroundColor(Color.black)
Spacer()
Button(action: {
let gotNextSpot = self.annotationModel.getNextAnnotation(index: self.nextAnnotation)
if gotNextSpot {
self.nextAnnotation += 1
} else {
self.nextAnnotation = -1 // a bit dirty...
}
}) {
Text("Get Next Spot >")
}
}.background(Color.white)
.frame(width: 200, height: 250, alignment: .center)
.cornerRadius(10)
.offset(x: 0, y: 0)
}
}
VStack{
HStack{
Button(action: {
self.addNextAnnotation(address: "Spot \(Int.random(in: 1..<1000))", isSpotA: true)
}) {
Text("Add to Spot A")
}.frame(width: 200, height: 50)
Button(action: {
self.addNextAnnotation(address: "Spot \(Int.random(in: 1..<1000))", isSpotA: false)
}) {
Text("Add to Spot B")
}.frame(width: 200, height: 50)
}
Spacer().frame(height: 50)
}
}
}
}
/// add a random annotion to the map
/// - Parameter address: address description
func addNextAnnotation(address: String, isSpotA: Bool) {
var newAnnotation = MGLPointAnnotation(title: address, coordinate: .init(latitude: 37.7912434, longitude: -122.396267))
if !isSpotA {
newAnnotation = MGLPointAnnotation(title: address, coordinate: .init(latitude: 37.7914434, longitude: -122.396467))
}
/// append to @State var which is used in teh mapview
annotations.append(newAnnotation)
/// also add location to model for calculations
/// would need refactoring since this is redundant
/// i leave it like that since it is more a prove of concept
annotationModel.addLocationInModel(annotation: newAnnotation)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(AnnotationModel())
}
}
Still pretty rough around the edges but could be a starting point.
Previous Answer, maybe still useful for some one. Have a look at the commits in the repo to get to the code..
Here is a working demo of a MapBox view to add random annotations and when you select one of the annotations it loops through the annotation array and displays the selected annotation in a TextView:
This demo is based on the excellent MapBox demo app
To add the desired functionality i decided to work with @EnvironmentObject for the model. This helped a lot UIViewRepresentable, Combine
I pushed everything to github I didn't used your model but i think you could integrate your functionality into the AnnotationModel or also do the logic in the SwiftUI View.
Here is what i did:
Define the Model:
import SwiftUI
import Combine
/// Source of Truth
class AnnotationModel: ObservableObject {
var didChange = PassthroughSubject<Void, Never>()
@Published var selectedAnnotaion: String = "none"
}
Add the @EnvironmentObject to the SceneDelegate
let contentView = ContentView().environmentObject(AnnotationModel())
The tricky part is to connect it to the @EnvironmentObject with MapBox UIViewRepresentable. Check out the link from above for how it's done via the Coordinator
struct MapView: UIViewRepresentable {
@Binding var annotations: [MGLPointAnnotation]
@EnvironmentObject var annotationModel: AnnotationModel
let mapView: MGLMapView = MGLMapView(frame: .zero, styleURL: MGLStyle.streetsStyleURL)
// MARK: - Configuring UIViewRepresentable protocol
func makeUIView(context: UIViewRepresentableContext<MapView>) -> MGLMapView {
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ uiView: MGLMapView, context: UIViewRepresentableContext<MapView>) {
updateAnnotations()
}
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self, annotationModel: _annotationModel)
}
Init @EnvironmentObject in the MapBox Coordinator then you can use it the Class with contains all the MapBox delegates. In the didSelect delegate i can loop through the annotations from the @Binding in MapView and are set via the @State var in the SwiftUI further down.
final class Coordinator: NSObject, MGLMapViewDelegate {
var control: MapView
@EnvironmentObject var annotationModel: AnnotationModel
init(_ control: MapView, annotationModel: EnvironmentObject<AnnotationModel>) {
self.control = control
self._annotationModel = annotationModel
}
func mapView(_ mapView: MGLMapView, didSelect annotation: MGLAnnotation) {
guard let annotationCollection = mapView.annotations else { return }
/// cycle throu the annotations from the binding
/// @Binding var annotations: [MGLPointAnnotation]
for _annotation in annotationCollection {
print("annotation", annotation)
if annotation.coordinate.latitude == _annotation.coordinate.latitude {
/// this is the same annotation
print("*** Selected annoation")
if let hastTitle = annotation.title {
annotationModel.selectedAnnotaion = hastTitle ?? "no string in title"
}
} else {
print("--- Not the selected annoation")
}
}
Finally the SwiftUI View
import SwiftUI
import Mapbox
struct ContentView: View {
@EnvironmentObject var annotationModel: AnnotationModel
@State var annotations: [MGLPointAnnotation] = [
MGLPointAnnotation(title: "Mapbox", coordinate: .init(latitude: 37.791434, longitude: -122.396267))
]
@State private var selectedAnnotaion: String = ""
var body: some View {
VStack{
MapView(annotations: $annotations).centerCoordinate(.init(latitude: 37.791293, longitude: -122.396324)).zoomLevel(16).environmentObject(annotationModel)
VStack{
Button(action: {
self.addNextAnnotation(address: "Location: \(Int.random(in: 1..<1000))")
}) {
Text("Add Location")
}.frame(width: 200, height: 50)
Text("Selected: \(annotationModel.selectedAnnotaion)")
Spacer().frame(height: 50)
}
}
}
/// add a random annotion to the map
/// - Parameter address: address description
func addNextAnnotation(address: String) {
let randomLatitude = Double.random(in: 37.7912434..<37.7918434)
let randomLongitude = Double.random(in: 122.396267..<122.396867) * -1
let newAnnotation = MGLPointAnnotation(title: address, coordinate: .init(latitude: randomLatitude, longitude: randomLongitude))
annotations.append(newAnnotation)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(AnnotationModel())
}
}
Important to know is that the @State var sets the annotations in MapBox. The MapBox delegate loops through this array and sets the selectedText in the @EnvironmentObject.This is the connection between the MapBox delegate back to SwiftUI. You could also put the annotations in the @EnvironmentObject, i didn't do that because was already defined like that in the demo app...
Let me know if this helps. Had fun checking this out...
Upvotes: 1