1

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.

nickcoding
  • 305
  • 8
  • 35
  • do you have some more code to show? i did a lot with mapbox and also answered some mapbox questions. doesn't sound like a big problem. you need to store all your annotations somewhere to compare if the annotation is the selected one, then you can select your desired annotation with a delegate method. – Peter Pohlmann Sep 07 '20 at 12:14
  • @PeterPohlmann I added the code for my annotation class and went into some more detail--let me know if there's anything I can add to help. – nickcoding Sep 08 '20 at 12:26
  • 1
    i will have a look and let you know if i could solve it – Peter Pohlmann Sep 08 '20 at 12:55
  • i tried to find a solution based on this https://github.com/mapbox/mapbox-maps-swiftui-demo . I tried to implement your model via EnvironmentObject but that is not working with mapbox ?! It would work with a @State var and function inside the view but this not what u want i guess. Maybe i still have an inspiring moment... – Peter Pohlmann Sep 09 '20 at 09:12
  • @PeterPohlmann Well VModel is my geocoder and you don't have that but I figured all you need to do to test the model is make a bunch of annotations in the same spot on the map from the onset--is there anything I can explain/add to make this easier? – nickcoding Sep 09 '20 at 12:27
  • 1
    So i mace an example thats works basically but it uses no models at all, i will push it later today to github so you can have a look ... – Peter Pohlmann Sep 10 '20 at 09:07
  • @PeterPohlmann Alright, thanks Peter! – nickcoding Sep 10 '20 at 11:51

2 Answers2

1

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

enter image description here

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:

  1. Define the Model:

    import SwiftUI
    import Combine
    
    /// Source of Truth
    class AnnotationModel: ObservableObject {
     var didChange = PassthroughSubject<Void, Never>()
     @Published var selectedAnnotaion: String = "none"
    }
    
  2. Add the @EnvironmentObject to the SceneDelegate

     let contentView = ContentView().environmentObject(AnnotationModel())
    
  3. 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)
        }
    
  4. 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")
    }
    }
    
  5. 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...

enter image description here

Peter Pohlmann
  • 1,478
  • 15
  • 30
  • I'm looking for some more along the lines of the annotations have the same coordinate and you can cycle through the callouts by essentially selecting each one through some callout accessory view or something like that – nickcoding Sep 11 '20 at 11:50
  • Yeah, i misread the question actually, so here is an updated answer, checkout if this works for you. the callout is done with a swiftui view, gives you more design possibilities. i am not shure if this would also work with the mapbox callout./// – Peter Pohlmann Sep 11 '20 at 18:36
  • Check my answer sir – nickcoding Sep 14 '20 at 14:44
  • 1
    Glad you could solve your problem. So did my answer help you with our own solutions ? thanks for the bounty. Was quite fun to work with MapBox again.... Good luck with your project... – Peter Pohlmann Sep 14 '20 at 17:20
  • I didn't use it for this specific problem but I might be able to incorporate it in the future to my project--thanks – nickcoding Sep 15 '20 at 01:46
0

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.

nickcoding
  • 305
  • 8
  • 35