jjackson-esristaff

Swift Place Finding Suggestions

Blog Post created by jjackson-esristaff Employee on Jul 24, 2014

One of the nifty newer features of the ArcGIS world geocoding service is the ability to provide search suggestions very quickly. Apps like Explorer for ArcGIS use this feature to provide suggestions as the user types into the search bar. Currently, the iOS Runtime SDK doesn’t expose the new capability, but it’s still fairly straight forward to add suggestions to your app.

 

IMG_2344.png

 

Extending the Locator

The AGSLocator class lets you geocode addresses, find places, and reverse-geocode locations. It provides a client side abstraction to interact with the world geocoding service hosted on ArcGIS Online or on-premise with ArcGIS Server. It's this service that now offers the suggest endpoint. 

 

The first step to extending AGSLocator is to create a Swift file in your project. You can name this AGSLocator.swift. You'll need to import the ArcGIS framework, then define an extension to hold the new methods.

 

import ArcGIS

 

extension AGSLocator {

 

}

 

The first thing we need to tackle is the suggest endpoint. The suggest endpoint lets you provide text and some optional parameters like location and distance, then returns back suggestions. For the full description of the REST endpoint check out suggest—ArcGIS REST API: World Geocoding Service | ArcGIS for Developers.

 

The approach we are going to take is to use an AGSJSONRequestOperation which is a convenience object provided by the SDK to simplify the process of making a web service request that passes along JSON. A large part of the iOS Runtime SDK makes use of this class. The body of our method will look something like this:

 

        let paramsDictionary = NSMutableDictionary(objectsAndKeys: "json", "f")

        paramsDictionary.addEntriesFromDictionary(params.encodeToJSON())

     

        let op = AGSJSONRequestOperation(URL: self.URL, resource: "suggest", queryParameters: paramsDictionary)

        op.securedResource = self

        op.requestCachePolicy = self.requestCachePolicy

        op.timeoutInterval = self.timeoutInterval

     

        op.completionHandler = { (obj: AnyObject!) -> Void in

            // TODO - deal with the json result, a dictionary

        }

     

        op.errorHandler = { (err: NSError!) -> Void in

            // TODO - deal with the error

        }

     

        // queue up the operation

        //

        AGSRequestOperation.sharedOperationQueue().addOperation(op)

 

The first step is to take the parameters which will be the same AGSLocatorFindParameters class used for other AGSLocator methods and serialize them to JSON so we can pass them to the AGSJSONRequestOperation initializer. Then we copy over the settings of the AGSLocator class (self) to the operation and setup the completion handlers. Finally we queue up the operation.

 

So now we need to decide how clients will call this new method. Since it's an asynchronous call we need to choose a pattern for the callback. The rest of the AGSLocator class uses a delegation pattern, but to be more Swift-like, we can use a closure.

 

The other thing we need to consider is the results of the call. Notice that the completionHandler gets passed a generic object (AnyObject) which is actually an NSDictionary containing the parsed JSON payload. Clients probably don't want to deal with that directly so let's introduce an abstraction that represents a suggestion, AGSLocatorSuggestion.

 

class AGSLocatorSuggestion : NSObject, AGSCoding

    var text: String!

    var magicKey: String!

    var isCollection : Bool = false

}

 

Note that our new class conforms to the AGSCoding protocol. I left out the implementation of those methods which deal with encoding and decoding JSON. With this new class to represent the result, we can define our new suggest method like this:

 

    func suggestionsForParameters(params: AGSLocatorFindParameters, completion: (results: [AGSLocatorSuggestion]?, error: NSError?) -> Void)

 

Clients simply provide a params object and a closure and the closure gets called with either an array of suggestions, or an error. Here's the full source for the method:

 

    func suggestionsForParameters(params: AGSLocatorFindParameters, completion: (results: [AGSLocatorSuggestion]?, error: NSError?) -> Void) {

     

        let paramsDictionary = NSMutableDictionary(objectsAndKeys: "json", "f")

        paramsDictionary.addEntriesFromDictionary(params.encodeToJSON())

     

        let op = AGSJSONRequestOperation(URL: self.URL, resource: "suggest", queryParameters: paramsDictionary)

        op.securedResource = self

        op.requestCachePolicy = self.requestCachePolicy

        op.timeoutInterval = self.timeoutInterval

     

        op.completionHandler = { (obj: AnyObject!) -> Void in

 

            var suggestions = [AGSLocatorSuggestion]()

            if let json = obj as? NSDictionary {

                if let array = json["suggestions"] as? [NSDictionary] {

                    for suggestionJson in array {

                        suggestions.append(AGSLocatorSuggestion(JSON: suggestionJson))

                    }

                }

            }

            completion(results: suggestions, error: nil)

        }

     

        op.errorHandler = { (err: NSError!) -> Void in

            completion(results: nil, error: err)

        }

     

        AGSRequestOperation.sharedOperationQueue().addOperation(op)

    }

 

I've added the code in the completion handler that decodes the JSON dictionary using the AGSLocatorSuggestion initializer.

 

Using the New Method

To use this new AGSLocator method the client simply needs to create an AGSLocatorFindParams object, populate it, then pass it in:

 

    func findSuggestions(searchText: String) {

        let params = AGSLocatorFindParameters()

        params.text = searchText

        params.maxLocations = 5

        params.outFields = ["*"]

        params.location = mapView.visibleAreaEnvelope.center

        params.distance = 5000

      

        lastSearch = searchText

      

        locator.suggestionsForParameters(params, { (results: [AGSLocatorSuggestion]?, error: NSError?) -> Void in

            self.suggestions = results

            self.tableView.reloadData()

        })

    }

 

Here we are not only providing the text for the suggestions, but also some information about the geographic context - namely the center point of the map and a radius of 5 kilometers. That information will help the geocoding service tailor the results based on what we're currently looking at.

 

What is a Suggestion?

The results returned are AGSLocatorSuggestion instances. The class that we introduced has three properties - text, magicKey, and isCollection. The text is the human readable text that you can use to show the suggestion to a user. In the example above the text from the first suggestion is "Gotham, England, United Kingdom".

 

The magicKey is a unique identifier provided by the server so that it can quickly retrieve the corresponding result at a later time. We'll see how we use this shortly.

 

The isCollection property indicates if the suggestion represents a specific place or a collection of places. If isCollection is false, the suggestion represents a single place like 100 Commerical St., Portland, ME. If isCollection is true it means the suggestion represents a search term for a common place name or a category of things. For example, the suggestion for a term like Coffee will likely be a collection of nearby coffee shops.

 

So retrieving the suggestions is the first step. The next step is to use one of the suggestions to actually retrieve locations.

 

Finding Locations with a Suggestion

Using the suggestion result to find locations is relatively simple. We can use the existing method, findWithParameters. The challenge is that we need to augment the AGSLocatorFindParameters class to introduce a new property, the magicKey. When the service responds to a find request and the magicKey is included in the parameters, it can quickly find the result that it cached when we made the call to suggest. So how do we add the magicKey to the existing class?

 

The most straight forward approach is to derive a new class that adds the magicKey property. The key is to override the JSON serialization methods so that when the AGSLocator prepares the operation corresponding to the find request, all of the appropriate values are present.

 

class AGSLocatorMagicFindParameters : AGSLocatorFindParameters {

   

    var magicKey : String!

   

    init() {

        super.init()

    }

   

    override func decodeWithJSON(json: [NSObject : AnyObject]!) {

        super.decodeWithJSON(json)

        magicKey = json["magicKey"]! as String

    }

   

    override func encodeToJSON() -> [NSObject : AnyObject]! {

        var result = NSMutableDictionary(dictionary: super.encodeToJSON())

        result["magicKey"] = magicKey

        return result

    }

}

 

Now, when we use findWithParameters we can include an AGSLocatorMagicFindParameters instance instead of the more mundane AGSLocatorFindParameters.

 

        let params = AGSLocatorMagicFindParameters()

        params.magicKey = suggestion.magicKey

        params.text = suggestion.text

        params.outSpatialReference = mapView.spatialReference

        params.maxLocations = suggestion.isCollection ? 5 : 1

       

        locator.delegate = self

        locator.findWithParameters(params)

 

Note that when we use the locator here we are using the more traditional delegation pattern. I'll leave it as an exercise for the reader to enhance the AGSLocator extension with a version of findWithParameters that takes a Swift closure instead. (Or maybe that can be the subject of another blog post.)

 

Conclusion

That's pretty much it. Once you retrieve AGSLocatorFindResult objects from findWithParams it's no different that before. If you'd like to see this in a working app, checkout my swift-samples repo here: jeffjax/swift-samples · GitHub

 

The sample is called Suggest. Let me know if you have any suggestions of your own.

Outcomes