0
votes

I am building an iOS application that involves using different MapBox image markers to illustrate the locations of different restaurants.

like this (accept I want multiple images): https://docs.mapbox.com/ios/assets/maps-sdk-image-annotation-example-480-8ce20af3475bd6b27381fda012a5b10b.webp

what I am currently doing:

I am currently typing in the coordinates of 10 different restaurants around my local town and displaying one logo on the screen (pisa logo from MapBox).

func mapView(_ mapView: MGLMapView, imageFor annotation: MGLAnnotation) -> MGLAnnotationImage? {
        // Try to reuse the existing ‘pisa’ annotation image, if it exists.
        var annotationImage = mapView.dequeueReusableAnnotationImage(withIdentifier: "pisa")

        if annotationImage == nil {
            // Leaning Tower of Pisa by Stefan Spieler from the Noun Project.
            var image = UIImage(named: "pisavector")!

            image = image.withAlignmentRectInsets(UIEdgeInsets(top: 0, left: 0, bottom: image.size.height/2, right: 0))

            // Initialize the ‘pisa’ annotation image with the UIImage we just loaded.
            annotationImage = MGLAnnotationImage(image: image, reuseIdentifier: "pisa")
        }

        return annotationImage
    }

what I want to achieve:

I want to store the separate coordinates and images on firebase's database so when I acquire a new restaurant, I can add the coordinates and images on firebase so that the map can be updated without updating the map.

what I need help with:

I need help with creating a function(s) that will allow me to input the array of coordinates and image URL's from each restaurant and display like the link above.

Any help is welcome :)

*******EDIT******* Would this be possible with Mapbox GEO JSON

1

1 Answers

2
votes

I accomplished something similar with func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView?. For reference, this is how my implementation of Mapbox looks:

my app

I created a custom UIView called EventImageView that had 2 UIImageViews, one displayed a pin while the other laid an icon image over the pin that communicated what the event was (for you, this could be a fries icon for fast food, or noodles for Asian restaurants. There are also functions in here for making the pin animate into place, expanding from a size of CGSize.zero to the appropriate pin size.

import Foundation
import Mapbox
import MaterialComponents

// MGLAnnotationView subclass
class CustomEventAnnotationView: MGLAnnotationView {

    //The parent view controller (in this case always a mapVC)
    var parentVC: MapViewController!
    //The imageview that has the colored pin
    var pinImageView = UIImageView()
    //The image view above the pin image view that shows us the category icon
    var categoryImageView = UIImageView()
    //The activity indicator for when the categoryImageView is loading
    var activityIndicator = MDCActivityIndicator()
    var categoryImage: UIImage!
    var hasLoaded = false

    override func layoutSubviews() {
        //Set the offset
        centerOffset = CGVector(dx: 0, dy: -20)

        //Set the frames of the subviews
        pinImageView.frame = CGRect(x: self.bounds.midX, y: self.bounds.maxY, width: 0, height: 0)
        categoryImageView.frame = pinImageView.frame
        activityIndicator.frame = CGRect(x: categoryImageView.frame.minX + 8, y: categoryImageView.frame.minY - 16, width: categoryImageView.frame.width - 15, height: categoryImageView.frame.height - 15)
        activityIndicator.isHidden = true
        activityIndicator.cycleColors = [UIColor.hootLightBlue]
        addSubview(activityIndicator)

        //Run growpin, which animates the pin growing from invisible to full size
        growPin()
    }

    override func prepareForReuse() {
        hasLoaded = false
    }

    func loadPinImageView(with imageNamed: String) {
        pinImageView.image = UIImage(named: imageNamed)
        self.addSubview(pinImageView)
    }

    func loadCategoryImageViewFromPreload(with image: UIImage) {
        categoryImageView.image = image
        self.addSubview(categoryImageView)
    }

    //Loads the category image view from a request to the url (this is called if the image hasn't already been preloaded)
    func loadCategoryImageViewFromRequest(with url: String) {
        if categoryImage == nil {
            //Social, job, commerce, and misc have their icons loaded onto the device, so we will pass their names in the url parameter
            switch url {
            case "Social":
                self.categoryImageView.image = UIImage(named: "social")
            case "Job":
                self.categoryImageView.image = UIImage(named: "job")
            case "Commerce":
                self.categoryImageView.image = UIImage(named: "commerce")
            case "Misc":
                self.categoryImageView.image = UIImage(named: "misc")
            default:
                //If the category is neither social, job, commerce, nor misc, then we show and animate the activity indicator...
                activityIndicator.isHidden = false
                activityIndicator.startAnimating()
                let task = URLSession.shared.dataTask(with: URL(string: url)!) {(data, response, error) in
                    //...request the image from the respective url
                    //Load the image from the data
                    let image: UIImage = UIImage(data: data!)!
                    DispatchQueue.main.async {
                        //Then set the image we received then hide the activityIndicator on the main thread
                        self.categoryImageView.image = image
                        self.activityIndicator.stopAnimating()
                        self.activityIndicator.isHidden = true
                    }
                }
                task.resume()
            }
        }
    }

    func growPin() {
        //This is the animation that grows the views from a size of CGPoint(height: 0, width: 0) to their respective heights
        self.addSubview(self.categoryImageView)
        UIView.animate(withDuration: 0.25) {
            self.pinImageView.frame = self.bounds
            self.categoryImageView.frame = CGRect(x: self.bounds.minX + 11, y: self.bounds.minY + 6, width: self.bounds.width - 22, height: self.bounds.width - 22)
        }
    }

    func shrinkPin(completion:@escaping (() -> Void)) {
        //Opposite of growPin, not called in code as of yet
        UIView.animate(withDuration: 0.25, animations: {
            self.pinImageView.frame = CGRect(x: self.bounds.midX, y: self.bounds.maxY, width: 0, height: 0)
            self.categoryImageView.frame = CGRect(x: self.bounds.midX, y: self.bounds.maxY, width: 0, height: 0)
        }) { (bool) in
            completion()
        }
    }
}

I also subclassed MGLAnnotation to make custom annotations that have a stored Event property. You could use custom restaurant objects in your custom annotation.

import Mapbox
class EventAnnotation: NSObject, MGLAnnotation {

    // As a reimplementation of the MGLAnnotation protocol, we have to add mutable coordinate and (sub)title properties ourselves.
    var coordinate: CLLocationCoordinate2D
    var title: String?
    var subtitle: String?
    var bundlePoint: CLLocationCoordinate2D?

    var event: Event!

    init(coordinate: CLLocationCoordinate2D, event: Event, title: String, subtitle: String) {
        self.coordinate = coordinate
        self.event = event
        self.title = title
        self.subtitle = subtitle
    }
}

I then use func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? within my MapViewController to create each annotation view:

import UIKit
import Mapbox
class MapViewController: UIViewController {
    ...
}

extension MapViewController: MGLMapViewDelegate {

    func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
        let eventAnnotation = annotation as? EventAnnotation

        //All annotation views with the same event category look the same, so we reuse them. 
        //If you have some sort of identifier for the type of restaurant each annotation represents, you'll want to use this restaurant identifier as your reuse identifier (assuming all pins for, say, Asian restaurants look the same)
        var reuseIdentifier: reuseIdentifier = eventAnnotation?.event.categories.first ?? ""

        // For better performance, always try to reuse existing annotations.
        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier) as? CustomEventAnnotationView

        // If there’s no reusable annotation view available, initialize a new one.
        if annotationView == nil {
            annotationView = CustomEventAnnotationView(reuseIdentifier: reuseIdentifier)
            annotationView!.bounds = CGRect(x: 0, y: 0, width: 40, height: 40)

            if let eventAnnotation = annotation as? EventAnnotation {
                for category in allCategories {
                    if category.id == eventAnnotation.event.categories.first {

                        //Return the correct image depending on what kind of event we're dealing with (social, job, commerce, misc)
                        switch category.type! {
                        case .social:
                            annotationView!.loadPinImageView(with:"redPin")
                        case .job:
                            annotationView!.loadPinImageView(with:"bluePin")
                        case .commerce:
                            annotationView!.loadPinImageView(with:"greenPin")
                        case .misc:
                            annotationView!.loadPinImageView(with:"grayPin")
                        }

                        //Actually load the image from the main thread
                        DispatchQueue.main.async {
                            if category.preloadedImage != nil {
                                    annotationView!.loadCategoryImageViewFromPreload(with: category.preloadedImage)
                            } else {
                                    annotationView!.loadCategoryImageViewFromRequest(with: category.imageURL ?? category.name)
                            }
                        }
                    }
                }
            }

            //This checks the rotation of the mapView.
            //The z-position of our annotation views need to be adjusted if, say, the top of the map points south and the bottom points north
            if mapView.direction > 90 && mapView.direction < 270 {
                annotationView?.layer.zPosition = CGFloat(90-(-(eventAnnotation?.coordinate.latitude)!))
            } else {
                if annotationView != nil && eventAnnotation != nil {
                    annotationView?.layer.zPosition = CGFloat(90-(eventAnnotation?.coordinate.latitude)!)
                }
            }
            return annotationView
        }
    }
}

It is important to note that I am adding my custom Event annotations to my MGLMapView. This is done by requesting the appropriate data, and once the asynchronous request for Event (or restaurant) data is complete, initializing the custom annotation via:

someAsyncRequestForEvents { (newEvents) in
    //newEvents is of type [Event]
    var newEventAnnotations: [EventAnnotation] = []
    for newEvent in newEvents {
        let newAnnotation = EventAnnotation(coordinate: CLLocationCoordinate2D(latitude: Double(newEvent.lat), longitude: Double(newEvent.long)), event: newEvent, title: newEvent.id!, subtitle: newEvent.id!)
        newEventAnnotations.append(newEvent)
    }
    mapView.addAnnotations(newEventAnnotations)
}

Sorry if this was sort of a lengthy response. I just know that it can be hard to get help on SO for specific frameworks like Mapbox for iOS. Figured I'd share some code and hopefully give you an idea of what you can do.