Aisle411 iOS MapSDK (Swift)

Introduction to Aisle411 iOS MapSDK (Swift)

The walkthrough on this page is for Swift 3/4. If you would like to see the Objective-C walkthrough, please see this page.

The Aisle411 iOS MapSDK allows for the a developer to user all of the tools provided by Aisle411's product and user location technologies. Below you will find a tutorial, along with several XCode Projects that will help ease you into using Aisle411's tools to display a store map, perform searches for both single and shopping lists of products, as well as display user location and route the user to his or her destination within the store.

Phase One - Initial Setup

Download MapSDK Files

The first thing that we need to do is to download the files for the Aisle411 MapSDK for iOS. You will need to select one of the following two versions:

Create new iOS project

Select Single View Application. Name your project accordingly on the following screen. The images below may be of aid to you.

Screenshot Screenshot

Add Linked Frameworks and Libraries

Click on app project file. Go down to Linked Frameworks and Libraries in the General tab. The images below may be of aid to you.

Screenshot Screenshot

Click the + sign and add the following files:

  • libz.tbd
  • libstdc++.6.0.9.tbd
  • CoreLocation.framework
Screenshot

The MapSDK is a framework too! Find the location that you saved the MapSDK and drag it into the Frameworks folder in XCode. You should see a popup that looks like the following image. Make sure that "Copy items if needed" and "Create groups" are selected.

Screenshot

Creating a Bridging Header

Go to File in the top left. Hover over New, and select File...

Screenshot

Then select C File. You can name it anything on the following page, this file is only temporary. Leave "Also create header file" unchecked. After you create the file, you will see a prompt that asks if you would lie\ke to create a bridging header. Select "Create Bridging Header".

Screenshot

Screenshot

This bridging header will allow for your Swift code to communicate with the Objective-C based Aisle411 MapSDK, as well as any other Objective-C libraries that you may later decide to import. Once the bridging header is created, you may delete the C file that you created in the previous step. We only did this as an easy way to generate a bridging header for your project.

The necessary files that we need to import in our briging header are as follows:


#import "MapBundle.h"
#import "MapController.h"
#import "MapBundleParser.h"
#import "OverlayItem.h"
#import "ProductCalloutOverlay.h"
#import <CommonCrypto/CommonCrypto.h>
				    

Building the Storyboard

Setting Up The View Controller

Click on the Main.storyboard file in the navigator on the left. On the right panel, at the bottom you should see a search bar. Click on the circular icon above this area, then search “View”. Drag this View into your View Controller.

Screenshot

Screenshot

While highlighting the view, go to the bottom right, to the left of the search bar, and press the square button with two vertical lines. Fill in 0’s for the four boxes at the top of the prompt. Make sure that the bars are highlighted in red. Then click Add 4 Constraints. This will make your view take up the entire screen of your View Controller.

Screenshot

Rename your new view to “mapView”. Select your ViewController, go the top top bar of XCode, click Editor, go down to Embed In, and select Navigation Controller.

Screenshot

Overhead Code

The following two sections will cover all "overhead" code in the two ViewController files that are required for the functions that follow to run successfully. The files will be posted all at once with comments to tag out certain portions that will be talked about below.

Overhead in ViewController.swift

Without further ado, here's ViewController.swift:

				    
//Overhead in ViewController
import UIKit


// These create Product and Section objects to more easily use the data that will be received from HTTP calls to the Aisle411 Webservices.
struct Section {
    let item_location_id: Int
    let map_location_id: Int
    let item_sub_location_id: Int
    let aisle: String
    let section: String
}

struct Product {
    let synonym_id: Int
    let synonym_nm: String
    let name: String
    let sections: [Section]
}

//These functions assists in parsing JSON responses from Aisle411 Webservices.
func convertToDictionary(text: String) -> [String: Any]? { //note2
    if let data = text.data(using: .utf8) {
        do {
            return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
        } catch {
            print(error.localizedDescription)
        }
    }
    return nil
}

func convertProducts(text: String) -> [String: AnyObject]? {
    if let data = text.data(using: .utf8) {
        do {
            return try JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject]
        } catch {
            print(error.localizedDescription)
        }
    }
    return nil
}

//This nearly automates the parsing of products returned by the Aisle411 Webservices.
extension Product {
    init?(json: [String:Any]){
        print(json)
        guard let name = json["name"] as? String,
            let synonym_id = json["synonym_id"] as? Int,
            let synonym_nm = json["synonym_nm"] as? String,
            let sectionsJSON = json["sections"] as? [AnyObject]
            else {
                return nil
        }
        var sections: [Section] = []
        for part in sectionsJSON {
            let section = Section(item_location_id: part["item_location_id"] as! Int, map_location_id: part["map_location_id"] as! Int,
                                  item_sub_location_id: part["item_sub_location_id"] as! Int, aisle: part["aisle"] as! String,
                                  section: part["section"] as! String)
            sections.append(section)
        }
        self.synonym_id = synonym_id
        self.synonym_nm = synonym_nm
        self.name = name
        self.sections = sections
    }
}

//Extension that checks if strings are numeric. Used for UPC searches.
extension String  {
    var isNumber : Bool {
        get{
            return !self.isEmpty && self.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) == nil
        }
    }
}

//Easy URL encoding and decoding
extension String
{
    func encodeUrl() -> String
    {
        return self.addingPercentEncoding( withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!
    }
    func decodeUrl() -> String
    {
        return self.removingPercentEncoding!
    }
    
}

//creates MD5 hashes from strings to be used in the webservice calls.
func MD5(string: String) -> Data {
    let messageData = string.data(using:.utf8)!
    var digestData = Data(count: Int(CC_MD5_DIGEST_LENGTH))
    
    _ = digestData.withUnsafeMutableBytes {digestBytes in
        messageData.withUnsafeBytes {messageBytes in
            CC_MD5(messageBytes, CC_LONG(messageData.count), digestBytes)
        }
    }
    
    return digestData
}

class ViewController: UIViewController {
    
    var productCallOutOverlay: ProductCalloutOverlay! //used to show product puns onscreen
    
    var mapController: MapController! //setup for displaying a map
    var mapBundle: MapBundle!
    var mapParser: MapBundleParser!
    var activeMap: Int = 0

    //called when the ViewController loads, loads map
    override func viewDidLoad() { 
        super.viewDidLoad()
        self.loadMap()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    //a wrapper around getStoreMap, initalizes variables for pins to be shown
    func loadMap(){
        self.activeMap = 1141489
        self.getStoreMap(storeId: self.activeMap)
        self.productCallOutOverlay = ProductCalloutOverlay()
        self.productCallOutOverlay.showChevronInCallout = true
        self.productCallOutOverlay.delegate = self
    }

//extends ViewController as a callout overlay delegate. 
    extension ViewController: CalloutOverlayDelegate {
    //occurs when an item is clicked
    func calloutOverlay(_ overlay: CalloutOverlay!, didItemSelected item: OverlayItem!) {
        
    }
    //occurs when an item is unselected
    func calloutOverlay(_ overlay: CalloutOverlay!, didItemDeselected item: OverlayItem!) {
        
    }
    //occurs when a new item is selected, or an item is reselected
    func calloutOverlay(_ overlay: CalloutOverlay!, didItemReselected oldItem: OverlayItem!, with item: OverlayItem!) {
        
    }
}
                                 

Exploring the Webservices

Quick Introduction

Use of the Aisle411 Webservices requires a partner id and secret to be passed in as parameters to the HTTP call. Contact Aisle411 at tech (at) aisle411 (dot) com to enquire about obtaining a partner id and secret for your company's use. However, for this sample app, we will give you a pairing to use. We will use partner id = 71 and secret is '(Cyp<r7MZ=8]=a' for this project. The Sample Grocery Store has retailer_store_id = 1141489. This will be the store_id passed into the web services.

Using the webservices will require HTTP calls to be made from your project. If you project does not have a valid SSL Certification, then the following steps will be necessary to allow your project to still make these calls.

  • First, go to the Info tab of your project by clicking on the .xcodeproj file and clicking the Info header. Your screen should look something like this: Screenshot
  • Next, hover over Bundle OS Type Code, and press the plus sign that appears. Screenshot
  • A new row will appear. Where it says Application Category, scroll up and select App Transport Security Settings. Screenshot
  • Finally, hit the arrow next to App Transport Security Settings. Then press the plus sign next to App Transport Security Settings and select Allow Arbitrary Loads. Lastly, go to the right of the row and click the arrow on the far right to toggle NO to YES. Screenshot

Now you will be able to make the calls to the Aisle411 webservices from your project. Let's get into those webservices tool now!

Displaying a Store Map

Let’s begin with Get Store Map. This is the webservice call that will be used to obtain a .imap file in order to load into your app. First we will create a function called getStoreMap that takes in an Int parameter called storeId, which corresponds to the retailer_store_id of the target store.

The process of downloading a map takes time and data, therefore, we want to minimize the amount of times that we need to actually download a map. We will use a basic caching system that will check to see if the map that you need already exists locally, and if so, we will load that instead of downloading a new one. This obviously has some shortcomings, like if the map or store data receive updates, but for this simple example, this should suffice. The following logic is the beginning of this process of checking for a cached map.


//getmap 1
    func getStoreMap(storeId: Int){
        let documentsUrl:URL = FileManager.default.urls(for: .documentDirectory, in:
            .userDomainMask).first as URL!
        let paths:NSArray = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) as NSArray
        let documentsDirectory = paths.object(at: 0) as! NSString
        let folderPath = documentsDirectory.appendingPathComponent("StoredMaps")
        let fileManager = FileManager()
        if fileManager.fileExists(atPath: folderPath) {}//if directory exists, continue
        else { //else, try to create the directory
            print("Directory not found.")
            do {
                try FileManager.default.createDirectory(atPath: folderPath, withIntermediateDirectories: false,attributes:  nil)
            }   catch let error as NSError {
                print(error.localizedDescription);
            }
        }
                                  

This code essentially checks to see if the map containing folder exists locally. If it does not, we will try to create the directory ourselves.


//getmap 2
        let destinationFilePath =
            documentsDirectory.appendingPathComponent("StoredMaps/\(storeId).imap")
        let destinationFileUrl =
            documentsUrl.appendingPathComponent("StoredMaps/\(storeId).imap")
        if fileManager.fileExists(atPath: destinationFilePath) {
            print("Found map file.")
            DispatchQueue.global().async {
                self.mapParser = MapBundleParser(pathToArchive: destinationFilePath)!
                self.mapBundle = self.mapParser.parse()
                DispatchQueue.main.async {
                    self.mapController = MapController()
                    self.mapController.mapBundle = self.mapBundle
                    self.mapController.view.frame = self.mapView.bounds //self.mapView.bounds
                    self.mapController.floorLevel = 1
                    self.mapController.setZoomButtonsHidden(true)
                    self.mapController.setCompassEnabled(false)
                    self.mapController.logoPosition = AisleLogoRightBottomPosition
                    self.mapView.addSubview(self.mapController.view)
                }
            }
        }
                                  

Now, whether the folder existed before or not, we set a destination where we expect the map file to be. We then check to see if it is indeed there: if so, we load the map locally, as seen in the lines embedded in the DispatchQueue, if not, we have to make our webservice call in order to acquire the file.

This following portion is the beginning of the else from the if above, meaning if the map is not found locally, that we do make the webservice call to acquire it. This portion of code sets variables necessarily to establish the proper URL for the webservice call. This is just the string manipulation portion to achieve a valid url, and the initial setup of the URLSession, SessionConfiguration, and URL objects.


//getmap 3
        else {
            let func_nm = "map"
            let part_id = 71
            let secret = "(Cyp<r7MZ=8]=a"
            let base = "https://aisle411.ws/webservices3/\(func_nm).php?"
            let lat = 38.6256740
            let long = -90.1892740
            let store_id = storeId
            var sendurl = ""
            let uappend = "\(func_nm)?latitude=\(lat)&longitude=\(long)&partner_id=\(part_id)&retailer_store_id=\(store_id)&\(secret)"
            let jumbleData = MD5(string:uappend)
            let jumble = jumbleData.map { String(format: "%02hhx", $0) }.joined()
            let auth = "auth=\(jumble)"
            let finappend = "latitude=\(lat)&longitude=\(long)&partner_id=\(part_id)&retailer_store_id=\(store_id)&\(auth)"
            sendurl = base + finappend
	    let url = URL(string: sendurl)
            let sessionConfig = URLSessionConfiguration.default
            let session = URLSession(configuration: sessionConfig)
                                  

This final portion handles the actual http call, handling of the response, and saving the received .imap file in the proper location to be used in the future. The portion in the DispatchQueue actually handles the loading of the map into the mapView that we set in the Storyboard earlier.


//getmap 4
            let task = session.downloadTask(with: request) { (tempLocalUrl, response, error) in
                if let tempLocalUrl = tempLocalUrl, error == nil {
                    if let statusCode = (response as? HTTPURLResponse)?.statusCode {
                        print("Successfully downloaded. Status code: \(statusCode)")
                    }
                    do {
                        try FileManager.default.copyItem(at: tempLocalUrl, to: destinationFileUrl)
                    } catch {
                        print("Failed to copy file")
                    }
                    if (fileManager.fileExists(atPath: destinationFilePath)) {
                        DispatchQueue.global().async {
                            self.mapParser = MapBundleParser(pathToArchive: destinationFilePath)!
                            self.mapBundle = self.mapParser.parse()
                            DispatchQueue.main.async {
                                self.mapController = MapController()
                                self.mapController.mapBundle = self.mapBundle
                                self.mapController.view.frame = self.mapView.bounds //self.mapView.bounds
                                self.mapController.floorLevel = 1
                                self.mapController.setZoomButtonsHidden(true)
                                self.mapController.setCompassEnabled(false)
                                self.mapController.logoPosition = AisleLogoRightBottomPosition
                                self.mapView.addSubview(self.mapController.view)
                            }
                        }
                    }
                    else {
                        print("Error: Failed to load map downloaded from webservices.")
                    }
                }
            }
            task.resume()
        }
    }
                                  

With all four portions of this code, the getStoreMap function will properly function, albeit, there is some overhead that is missing at this point, but will be talked about later. If you download the entire sample, all of the overhead is already present.

Searching For a Product

Now that we can successfully display a map, let’s move on to performing searches on individual items, and dropping pins on the map for them. The webservice that we will use to achieve this is our Single Product Search.

You can download the project files for this section below. Choose the download that matches your testing environment:


We will pass two parameters into the function singleProductSearch, called storeId, which corresponds to the retailer_store_id of the target store, and item, which is the string corresponding to an item in the store by term.

This following code snippet just establishes the parameters that will be passed into the webservice call. More information on these values can be found on the Single Product Search documentation page.


//searchproduct 1
    func singleProductSearch(storeId: Int, item: String){
        let func_nm = "searchproduct"
        let part_id = 71
        let secret = "(Cyp<r7MZ=8]=a"
        let base = "https://aisle411.ws/webservices3/\(func_nm).php?"
        let beg = 0
        let end = 1
        let lat = 38.6256740
        let upc = item.isNumber
        let long = -90.1892740
        let store_id = storeId
        let term = item
        var sendurl = ""
                                  

This second portion parses these above parameters into an appropriate url to use for the http request to the webservice. Notice that UPC and Term searches have a slightly different format, but are handled smoothly.


//searchproduct 2
	if (upc){
            let uappend = "\(func_nm)?end=\(end)&latitude=\(lat)&longitude=\(long)&partner_id=\(part_id)&retailer_store_id=\(store_id)&start=\(beg)&upc=\(upc)&\(secret)"
            let jumbleData = MD5(string: uappend)
            let jumble = jumbleData.map { String(format: "%02hhx", $0) }.joined()
            print("md5Hex: \(jumble)")
            let auth = "auth=\(jumble)"
            let finappend = "end=\(end)&latitude=\(lat)&longitude=\(long)&partner_id=\(part_id)&retailer_store_id=\(store_id)&start=\(beg)&upc=\(upc)&\(auth)"
            sendurl = base + finappend
            sendurl = sendurl.encodeUrl()
        }
        else {
            let tappend = "\(func_nm)?end=\(end)&latitude=\(lat)&longitude=\(long)&partner_id=\(part_id)&retailer_store_id=\(store_id)&start=\(beg)&term=\(term)&\(secret)"
            let jumbleData = MD5(string: tappend)
            let jumble = jumbleData.map { String(format: "%02hhx", $0) }.joined()
            print("md5Hex: \(jumble)")
            let auth = "auth=\(jumble)"
            let finappend = "end=\(end)&latitude=\(lat)&longitude=\(long)&partner_id=\(part_id)&retailer_store_id=\(store_id)&start=\(beg)&term=\(term)&\(auth)"
            sendurl = base + finappend
            sendurl = sendurl.encodeUrl()
        }
                                  

This portion below takes care of sending the http request, and also handles the overhead for actually parsing the response that we receive from the webservice call. By reading up on the documentation of Single Product Search, you will find that there are a variety of responses that we could receive, and we need to be ready for anything that comes back to us.


//searchproduct 3
        let url = URL(string: sendurl)
        let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in
            if let data = data {
                let response = (NSString(data: data, encoding: String.Encoding.utf8.rawValue) ?? "No response from request.")
                print(response)
                var parsedData = convertToDictionary(text: response as String)
                var productsExists = false
                var closeExists = false
                if (parsedData != nil) {
                    productsExists = parsedData?["products"] != nil
                    closeExists = parsedData?["product_suggestions"] != nil
                }
                var products: [FMProduct] = []
                                  

This portion below handles the parsing of products assuming that our search returned an exact match. The latter 5 lines handle making the product pins show up on the map using productCallOutOverlay. Product objects, Section objects, and MapViewController are all defined in the overhead section above. The portion that follows is the conclusion of the function, including the parsing of products that only returned close matches (almost identical to the first portion).


//searchproduct 4
                if (productsExists){
                    let obj = convertProducts(text: response as String)
                    let productarray = obj!["products"]!
                    let max = productarray.count
                    var count = 0
                    while (count < max){
                        let product = Product(json: productarray[count] as! [String : Any])
                        for section in product!.sections {
                            let temp = FMProduct()
                            let tempsection = FMSection(sublocation: Int32(section.item_sub_location_id), location: Int32(section.item_location_id))
                            temp.name = product!.name
                            temp.idn = Int32(product!.synonym_id)
                            let sectionlist : [String] = []
                            temp.sections = sectionlist
                            tempsection!.maplocation = Int32(section.map_location_id)
                            tempsection!.aisleTitle = section.aisle
                            tempsection!.title = section.section
                            temp.sections.append(tempsection!)
                            products.append(temp)
                        }
                        count += 1
                    }
                    self.productCallOutOverlay.products = products
                    DispatchQueue.main.async {
                        self.mapController.remove(self.productCallOutOverlay)
                        self.mapController.add(self.productCallOutOverlay)
                    }
                }

                                  

//searchproduct 5
                else if (closeExists){
                    let obj = convertProducts(text: response as String)
                    let productarray = obj!["product_suggestions"]!
                    let max = productarray.count
                    var count = 0
                    while (count < max){
                        let product = Product(json: productarray[count] as! [String : Any])
                        for section in product!.sections {
                            let temp = FMProduct()
                            let tempsection = FMSection(sublocation: Int32(section.item_sub_location_id), location: Int32(section.item_location_id))
                            temp.name = product!.name
                            temp.idn = Int32(product!.synonym_id)
                            let sectionlist : [String] = []
                            temp.sections = sectionlist
                            tempsection!.sublocation = Int32(section.item_sub_location_id)
                            tempsection!.aisleTitle = section.aisle
                            tempsection!.title = section.section
                            temp.sections.append(tempsection!)
                            products.append(temp)
                        }
                        count += 1
                    }
                    self.productCallOutOverlay.products = products
                    DispatchQueue.main.async {
                        self.mapController.remove(self.productCallOutOverlay)
                        self.mapController.add(self.productCallOutOverlay)
                    }
                }
            }
            else {
                print(error ?? "no error?")
            }
        }
        task.resume()
    }

                                   

Now that the logic is there, it is necessary for us to implement an actual search bar!. Go back to Main.storyboard and search for Search Bar in the bottom right and drag it to the ViewController. Make sure to resize your mapView so that there is enough room for the space bar. Ctrl + drag from the Search Bar into ViewController.swift, just like with the View above. Name this connection "searchBar". Return to ViewController.swift and add the following lines inside of the definition of ViewController:


    func searchBarTextDidBeginEditing(_ searchBar: UISearchBar){
        
    }
    
    func searchBarTextDidEndEditing(_ searchBar: UISearchBar){
        
    }
    
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar){
        if let search = searchBar.text {
            singleProductSearch(storeId: activeMap, item: search)
            searchBar.text = ""
        }
    }
    
    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        
    }
    
    func searchBarCancelButtonClicked(_ searchBar: UISearchBar){
        searchBar.text = ""
    }
                                  

Now we can both display store maps, and drop pins on the maps in response to user searches!

Performing a Shopping List Search

Let’s move forward with our Shopping List Search. This is a function very similar to Single Product Search, in that you pass in some data and get a product as a result. However, in this case, you can pass in a list of items, both UPC and terms if you'd like, and get results in a similar way as you did with a single product. The Aisle411 Webservice call requires a POST request with a well-formatted JSON shopping list; however, in the code that I will show below, I take care of creating that with Swift code, so your job becomes easy!

You can download the project files for this section below. Choose the download that matches your testing environment:

s

The setup for the shoppingListSearch function is very similar to the two above, so the first portion will be the expected variable initialization, as well as the formation of the POST request. I will comment more on the POST request below.


//listsearch 1
    func shoppingListSearch(storeId: Int, list: [String]){
        let devtok = UIDevice.current.identifierForVendor!.uuidString
        let func_nm = "locateitems"
        let part_id = 71
        let secret = "(Cyp<r7MZ=8]=a"
        let base = "https://aisle411.ws/webservices3/\(func_nm).php?";
        var body : [String:Any] = [:]
        let shoplist = buildShoppingList(list: list)
        body["device_token"] = devtok;
        body["partner_id"] = part_id;
        body["retailer_store_id"] = storeId;
        body["shopping_list_data"] = shoplist;
        body["latitude"] = 38.625674;
        body["longitude"] = -90.1892740;
        var sendbody : Data
        do {
            sendbody = try JSONSerialization.data(withJSONObject: body, options: .prettyPrinted)
        } catch {
            return
        }
        let sendstring = String(data: sendbody, encoding: String.Encoding.utf8) as String!
        let appendage = sendstring! + secret;
        let jumbleData = MD5(string: appendage)
        let jumble = jumbleData.map { String(format: "%02hhx", $0) }.joined()
        var request = URLRequest(url: URL(string: base)!)
        request.httpMethod = "POST"
        request.addValue(jumble, forHTTPHeaderField: "Authentication")
        request.httpBody = sendbody
                                  

This code initializes all of the variables needed to perform the POST request. Before I move forward, let's look at that buildShoppingList() function:


func buildShoppingList(list: [String]) -> [String:Any]{
        var shoplist: [String:Any] = [:]
        shoplist["name"] = "Default Shopping List"
        var items = [Any]()
        var count = 0
        for item in list {
            if item.isNumber {
                items.append(["upc": item])
            }
            else {
                items.append(["name": item.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)!])
            }
            count += 1
        }
        shoplist["items"] = items
        return shoplist
    }
                                  

This above function takes in a list of strings, which can be numerical for UPC searches. A mix of numerical and non-numerical strings is fine. We define a shopping list name, then compile a list of items of both term and UPC search based on the input list, then output the shopping list dictionary. This dictionary is then used in the POST request after being JSON-serialized, which will be talked about a bit more in the following paragraph.

The url for this request is simply the base string variable. The data in our POST consist of an Authentication header containing jumble, and a body containing a JSON-serialized dictionary of variables defined above. The next portion will actually execute this request and handle the results. The latter handling will be in separate functions, so those snippets will immediately follow with commentary.


//listsearch 2
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            guard let data = data, error == nil else {
                print("error=\(String(describing: error))")
                return
            }
            
            if let httpStatus = response as? HTTPURLResponse, httpStatus.statusCode != 200 {
                print("statusCode should be 200, but is \(httpStatus.statusCode)")
                print("response = \(String(describing: response))")
            }
            
            let responseString = String(data: data, encoding: .utf8)
            print("responseString = \(String(describing: responseString))")
            if responseString != nil {
                self.parseResults(results: responseString!)
            }
        }
        task.resume()
    }
                                  

Now, for the function parseResults:


func parseResults(results: String){
        let parsed = convertProducts(text: results)!
        var products: [FMProduct] = []
        let items = parsed["items"]! as! [Any]
        for item in items {
            let product = Product(json: item as! [String: Any])
            for section in product!.sections {
                let temp = FMProduct()
                let tempsection = FMSection(sublocation: Int32(section.item_sub_location_id), location: Int32(section.item_location_id))
                temp.name = product!.name.decodeUrl()
                temp.idn = Int32(product!.synonym_id)
                let sectionlist : [String] = []
                temp.sections = sectionlist
                tempsection!.maplocation = Int32(section.map_location_id)
                tempsection!.aisleTitle = section.aisle
                tempsection!.title = section.section
                temp.sections.append(tempsection!)
                products.append(temp)
            }
        }
        self.productCallOutOverlay.products = products
        DispatchQueue.main.async {
            self.mapController.remove(self.productCallOutOverlay)
            self.mapController.add(self.productCallOutOverlay)
        }
    }
                                  

This function uses convertProducts() as seen above in singleProductSearch() to make the JSON response from the HTTP call into a dictionary that we can work with. The items portion of this dictionary is then iterated over, building an FMProduct for each item, as again seen above in singleProductSearch. We then drop the pins for each item on the map, and set the shoppinglist variable equal to true. This just allows for the program to know that a shopping list search is being performed, rather than a single product search via the search bar.

Now, we are able to display a map, search for individual items, as well as perform a search over an entire shopping list!