title: Mapbox: add annotation only if the annotation is visible on screen
body: <p>I have the data for my annotations stored in a database on Firebase. I've discovered that I can download data for 10,000 annotations and add these annotations to my map without much of a lag so long as the annotations do not have a custom view.</p>
  
  <p>For my app however, I'm going to need to use custom views, each annotation view is an image composed of multiple image pieces. If I use custom views(even if the custom view is just a single UIImage), the app freezes up and eventually I receive the error "Message from debugger: Terminated due to memory issue". My app has a minimum zoom level of 15 so users can mostly only see what is around them.</p>
  
  <p>My goal is to download the annotation data for all the annotations within maybe 10 km of the user(I'll do this with geohashing although this is not the focus of this question). The map on the phone will only be able to view a span of land of about a km or so.</p>
  
  <p>I then either only want to</p>
  
  <pre><code> a) add annotations that are visible on the phone 
  </code></pre>
  
  <p>or</p>
  
  <pre><code>b) only load the views for the annotations that are visible.
  </code></pre>
  
  <p>I want the annotations to be visible as soon as they are within the boundaries of the screen though, so that if a user is scrolling through the map, they see these annotations immediately.</p>
  
  <hr>
  
  <p>I have this delegate function within my view controller that determines the view of each annotation, when I comment it out there is a slight delay is adding the annotations, but not a whole lot.</p>
  
  <pre><code>func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -&gt; MGLAnnotationView? {
      if annotation is MGLUserLocation &amp;&amp; mapView.userLocation != nil {
          let view = CurrentUserAnnoView(reuseIdentifier: currentUser.uid!)
          self.currentUserAnno = view
          return view
      }
      else if annotation is UserAnnotation{
          let anno = annotation as! UserAnnotation
          let auid = anno.reuseIdentifier //The anno uid
          if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: auid) {
              return annotationView
          } else {
              let annotationView = UserAnnotationView(reuseIdentifier: auid, size: CGSize(width: 45, height: 45), annotation: annotation)
              annotationView.isUserInteractionEnabled = true
              anno.view = annotationView
              return annotationView
          }
      }
      return MGLAnnotationView(annotation: annotation, reuseIdentifier: "ShouldntBeAssigned")  //Should never happen
  }
  </code></pre>
  
  <hr>
  
  <p><strong>Example</strong></p>
  
  <p>If you look at this youtube video, you can see that the annotations aren't always visible, and they only become visible as you zoom or move over them.<br>
  <a href="https://youtu.be/JWUFD48Od4M" rel="nofollow noreferrer">https://youtu.be/JWUFD48Od4M</a></p>
  
  <hr>
  
  <p><strong>MapViewController</strong></p>
  
  <pre><code>class MapViewController: UIViewController {
  
      @IBOutlet weak var newPostView: NewPostView!
      @IBOutlet var mapView: MGLMapView!
      var data: MapData?
      var currentUserAnno: CurrentUserAnnoView?
      var testCounter = 0
  
      let geoFire = GeoFire(firebaseRef: Database.database().reference().child("/users/core"))
  
      @IBAction func tap(_ sender: UITapGestureRecognizer) {
          self.view.endEditing(true)
  
      }
  
      override func viewDidAppear(_ animated: Bool) {
          super.viewDidAppear(animated)
          geoFire.setLocation(CLLocation(latitude: 37.7853889, longitude: -122.4056973), forKey: "7")
          self.startup()
      }
  
      func startup(){
          if CLLocationManager.isOff(){
              let popup = UIAlertController(title: "Location Services are Disabled", message: "Please enable location services in your 'Settings -&gt; Privacy' if you want to use this app", preferredStyle: UIAlertController.Style.alert)
              popup.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default, handler: {(alert: UIAlertAction) in
                  self.startup()
              }))
              popup.view.layoutIfNeeded()
              self.present(popup, animated: true, completion: nil)
          }else{
              self.mapView.userTrackingMode = .follow
              self.data = MapData(delegate: self)
          }
      }
  
      @IBAction func newHidea(_ sender: Any) {
          newPostView.isHidden = false
      }
  
  
  }
  
  extension MapViewController: MGLMapViewDelegate{
  
      func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -&gt; MGLAnnotationView? {
          print(testCounter)
          testCounter = testCounter + 1
          if annotation is MGLUserLocation &amp;&amp; mapView.userLocation != nil {
              let view = CurrentUserAnnoView(reuseIdentifier: currentUser.uid!)
              self.currentUserAnno = view
              return view
          }
          else if annotation is UserAnnotation{
              let anno = annotation as! UserAnnotation
  //            let auid = anno.reuseIdentifier //The anno uid
              if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: "UserAnnotationView") {
                  return annotationView
              } else {
                  let annotationView = UserAnnotationView(reuseIdentifier: "UserAnnotationView", size: CGSize(width: 45, height: 45), annotation: annotation)
                  annotationView.isUserInteractionEnabled = true
                  //anno.view = annotationView
                  return annotationView
              }
          }
          return MGLAnnotationView(annotation: annotation, reuseIdentifier: "ShouldntBeAssigned")  //Should never happen
      }
  
  
      func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -&gt; MGLCalloutView? {
      /*The regular anno status box is replaced by one with buttons*/
          let annotationPoint = mapView.convert(annotation.coordinate, toPointTo: nil)
          let viewFrame = CGRect(origin: CGPoint(x: 0, y: -10), size: CGSize(width: 180, height: 400))
          var cView: AnnoCalloutView
          if (annotation as! UserAnnotation).status != nil{
              cView =  StatusCallout(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
          }else{
              cView = ProfileCallout(representedObject: annotation, frame: viewFrame, annotationPoint: annotationPoint)
          }
          return cView
      }
  
      func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -&gt; Bool {
          if (annotation is UserAnnotation) {
              return true
          }else{
              return false
          }
  
      }
  
      func mapView(_ mapView: MGLMapView, tapOnCalloutFor annotation: MGLAnnotation) {
          mapView.deselectAnnotation(annotation, animated: true)  // Hide the callout.
      }
  
  
  
  }
  
  //TODO: Check if there's a better method than a delegate to do this, since it's Model -&gt; Controller
  extension MapViewController: MapDataDelegate{
      func addAnnotation(_ anno: UserAnnotation) {
          self.mapView?.addAnnotation(anno)
      }
  }
  </code></pre>
  
  <hr>
  
  <p><strong>UserAnnotation</strong></p>
  
  <pre><code>class UserAnnotation: NSObject, MGLAnnotation {
  
      //////////Ignore these, required for MGLAnnotation//////
      var title: String?
      var subtitle: String?
      ////////////////////////////////////////////////////////
  
      var coordinate: CLLocationCoordinate2D
      var status: Status?{
          didSet{
              //TODO: update annotation
          }
      }
      var reuseIdentifier: String
      var avatar: Avatar
      var uid: String
  
      //MARK: You could assign these when the profile is viewed once, so if they view it again you have it saved.
      var uName: String?
      var bio: String?
  
      init(coordinate: CLLocationCoordinate2D, avatar: Avatar, reuseIdentifier: String?, uid: String) {
  //    init(coordinate: CLLocationCoordinate2D, reuseIdentifier uid: String?) {
          self.coordinate = coordinate
          self.title = "None"
          self.subtitle = "None"
          self.reuseIdentifier = reuseIdentifier!
          self.uid = uid
          self.avatar = avatar
          super.init()
  //        self.setAvatar(avatar: avatar)
      }
  
      init(coordinate: CLLocationCoordinate2D, title: String?, subtitle: String?){
          print("This shouldn't be printing")
          self.coordinate = coordinate
          self.uName = "ShouldntBeSet"
          self.title = "ShouldntBeSet"
          self.subtitle = "ShouldntBeSet"
          self.reuseIdentifier = "ShouldntBeAssigned"
          self.uid = "ShouldntBeAssigned"
          self.avatar = Avatar(withValues: [0])
      }
  }
  </code></pre>
  
  <hr>
  
  <p><strong>UserAnnotationView</strong></p>
  
  <pre><code>class UserAnnotationView: MGLAnnotationView {
  
      var anno: UserAnnotation?
      var statusView: UITextView?
      var imageView: UIImageView?
      var avatarImage: UIImage{
          let ai = AvatarImage()
          ai.update(with: (anno?.avatar.values)!)
          return ai.image!
      }
  
  
      init(reuseIdentifier: String, size: CGSize, annotation: MGLAnnotation) {
          super.init(reuseIdentifier: reuseIdentifier)
          // Prevents view from changing size when view tilted
          scalesWithViewingDistance = false
          frame = CGRect(x: 0, y: 0, width: size.width, height: size.height)
          self.anno = annotation as? UserAnnotation
          self.setUpImageView(frame: frame, size: size, annotation: annotation)
          if anno?.status != nil{
              self.createStatus(status: (anno?.status?.status)!)
          }
      }
  
      func reuseWithDifferentAnno(annotation: UserAnnotation){
          self.anno = annotation
          self.imageView!.image = UIImage(named: "Will")
          //        let av = AvatarImage.newAvatar(values: (anno?.avatar.values)!)
  //        self.imageView!.image = av.image
  //        if anno?.status != nil{
  //            self.createStatus(status: (anno?.status?.status)!)
  //        }else{
  //            if statusView != nil{
  //                deleteStatus()
  //            }
  //        }
      }
  
      required init?(coder aDecoder: NSCoder) {
          super.init(coder: aDecoder)
      }
  
      private func setUpImageView(frame: CGRect, size: CGSize, annotation: MGLAnnotation){
          self.imageView = UIImageView(frame: frame)
          self.imageView!.translatesAutoresizingMaskIntoConstraints = false
          if annotation is UserAnnotation {
  //            let av = AvatarImage.newAvatar(values: (anno?.avatar.values)!)
  //            self.imageView!.image = av.image
              self.imageView!.image = UIImage(named: "Will")
  
          }else{
              let image = UIImage()
              self.imageView!.image = image
          }
          addSubview(self.imageView!)
          imageViewConstraints(imageView: self.imageView!, size: size)
      }
  
      func setImage(to image: UIImage){
          self.imageView!.image = image
      }
  
      func createStatus(status: String){
          if (status == self.statusView?.text) &amp;&amp; (self.subviews.contains(self.statusView!)){
              return
          }else if self.statusView != nil &amp;&amp; self.subviews.contains(self.statusView!){
              deleteStatus()
          }
          self.statusView = UITextView()
          self.statusView!.text = status
          self.statusView!.isHidden = false
          self.adjustUITextViewHeight()
          self.statusView!.translatesAutoresizingMaskIntoConstraints = false
          self.statusView!.layer.cornerRadius = 5
          self.statusView!.textAlignment = .center
          addSubview(self.statusView!)
          textViewConstraints(textView: self.statusView!, isAbove: self.imageView!)
      }
  
      func deleteStatus(){
          self.statusView?.removeFromSuperview()
          self.statusView = nil
      }
  
      private func adjustUITextViewHeight(){
  
          self.statusView!.translatesAutoresizingMaskIntoConstraints = true
          self.statusView!.sizeToFit()
          self.statusView!.isScrollEnabled = false
      }
  
      private func imageViewConstraints(imageView: UIImageView, size: CGSize){
          let widCon = NSLayoutConstraint(item: imageView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1, constant: size.width)
          let heightCon = NSLayoutConstraint(item: imageView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1, constant: size.height)
          let cenCon = NSLayoutConstraint(item: imageView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0)
          NSLayoutConstraint.activate([cenCon, widCon, heightCon])
      }
  
      private func textViewConstraints(textView status: UITextView, isAbove imageView: UIImageView){
          let cenCon = NSLayoutConstraint(item: status, attribute: .centerX, relatedBy: .equal, toItem: imageView, attribute: .centerX, multiplier: 1, constant: 0)
          let botCon = NSLayoutConstraint(item: status, attribute: .bottom, relatedBy: .equal, toItem: imageView, attribute: .top, multiplier: 1, constant: -10)
          let widCon = NSLayoutConstraint(item: status, attribute: .width, relatedBy: .lessThanOrEqual, toItem: nil, attribute: .width, multiplier: 1, constant: 200)
          NSLayoutConstraint.activate([cenCon, botCon, widCon])
      }
  
  }
  </code></pre>
  
  <hr>
  
  <p><strong>MapData</strong></p>
  
  <pre><code>class MapData {
      var annotations = [String:UserAnnotation]()
      var updateTimer: Timer?
      var delegate: MapDataDelegate
  
      init(delegate: MapDataDelegate){
          self.delegate = delegate
          self.startTimer()
      }
  
      @objc func getUsers(){
          FBCore.getAllUsers(completion:{(users) in
              for child in users {
                  let value = child.value as! NSDictionary
                  self.getDataFor(user: value, whoseUidIs: child.key)
              }
          })
      }
  
      func getDataFor(user: NSDictionary, whoseUidIs annoid: String){
          if annoid != currentUser.uid! &amp;&amp; (currentUser.blockedBy?[annoid] ?? false) != true &amp;&amp; (currentUser.blocks?[annoid] ?? false) != true{
              guard let (coord, status, avatar) = FBCoreUser.get(forQryVal: user)
                  else {return}
              if let anno = self.annotations[annoid]{
                  anno.coordinate = coord
                  if status != nil{// &amp;&amp; anno.view!.isSelected == false {
                      if ((status?.isExpired)!){
                          anno.status = nil
                      }else{
                          anno.status = status
                      }
                  }
                  if avatar.values != anno.avatar.values{
                      anno.avatar = avatar
                  }
              }else{
                  let anno = UserAnnotation(coordinate: coord, avatar: avatar, reuseIdentifier: "UserAnnotation", uid: annoid)
                  if status != nil{
                      if ((status?.isExpired)!){
                          anno.status = nil
                      }else{
                          anno.status = status
                      }
                  }
                  self.annotations[annoid] = anno
                  //print(anno.reuseIdentifier)
                  delegate.addAnnotation(anno)
              }
          }
      }
  
      func startTimer(){
          // Scheduling timer to Call the function "updateCounting" with the interval of 5 seconds
          if updateTimer != nil{
              updateTimer!.invalidate()
          }
          updateTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(getUsers), userInfo: nil, repeats: true)
      }
  }
  </code></pre>