Skip navigation
All People > jjackson-esristaff > Developing with ArcGIS > 2014 > July
2014

Developing with ArcGIS

July 2014 Previous month Next month

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.

jjackson-esristaff

Old School Enums

Posted by jjackson-esristaff Employee Jul 22, 2014

When working with the ArcGIS Runtime SDK and Swift we are occasionally reminded that the SDK was written in Objective-C. And while Swift generally does a nice job of bridging the two languages, there are some shortcomings. One of those is how Swift deals with enums.

 

For example, the enum AGSGeometryOffset type is defined in Objective-C like this:

 

typedef enum {

  AGSGeometryOffsetTypeMitered = 0,   /*!< Mietered */

  AGSGeometryOffsetTypeBevelled,      /*!< Bevelled  */

  AGSGeometryOffsetTypeRounded,       /*!< Rounded */

  AGSGeometryOffsetTypeSquare         /**  Square  */

} AGSGeometryOffsetType;

 

Swift turns that into this:

 

var AGSGeometryOffsetTypeMitered: AGSGeometryOffsetType { get }

/*!< Bevelled  */

var AGSGeometryOffsetTypeBevelled: AGSGeometryOffsetType { get }

/*!< Rounded */

var AGSGeometryOffsetTypeRounded: AGSGeometryOffsetType { get }

var AGSGeometryOffsetTypeSquare: AGSGeometryOffsetType { get } /**  Square  */

struct AGSGeometryOffsetType {

    init(_ value: UInt32)

    var value: UInt32

}

 

Note: If the Objective-C enum is defined with the NS_ENUM() macro, then Swift does a nice job of importing it; however, that's not the case for now with the ArcGIS SDK. We will be releasing an update to address this issue, but in the meantime there are some things you can do.

 

Before Beta-4, you were able to use the value property of the enum in order to do comparisons or switch statements. For example:

 

    let maneuver = someFunction()

   

    if maneuver.value == AGSGeometryOffsetTypeSquare.value {

        // do something

    }

 

But with Beta-4 which was released yesterday, that value property is no longer accessible. The error looks like this:

 

'AGSGeometryOffsetType' does not have a member named 'value'.

 

With some help from some developers on the Apple forums, I came up with this work-around. It's what I'm using in my Swift apps until we have support for the NS_ENUM() macro in the SDK. Use an extension on the enum type to introduce a new property that uses reinterpretCast to generate an integer value:

 

extension AGSGeometryOffsetType {

    var rawValue : UInt32 { get {

        var raw: UInt32 = reinterpretCast(self)

        return raw

    }}

}

 

Then use the property explicitly as before:

 

    let maneuver = someFunction()

   

    if maneuver.rawValue == AGSGeometryOffsetTypeSquare.rawValue {

        // do something

    }

 

It's no fun having to create all of those Swift extensions, but it's better than being stuck in the water until the SDK update is available.

Those of us in Esri that build apps for iOS and OS X are pretty excited about Apple's new programming language. Like you, we are trying our best to stay on top of the moving target that is Swift. I figure I can share some of our experiences to the wider ArcGIS developer community and hopefully spark some good discussion along the way.

 

Let's start out with a very simple tutorial that will get you started with an iOS app that uses Swift and ArcGIS. I'll follow up with some other posts which dive into more technical depth.

 

Augment the SDK

The ArcGIS SDK is written in Objective-C, but it works pretty well with Swift as is. In order to use it, the simplest thing you can do is add a module definition file to your SDK installation. The file should look like this:

 

framework module ArcGIS {

  umbrella header "ArcGIS.h"

  export *

  module * { export * }

}

 

The file should be named module.modulemap and it needs to be placed in a folder called Modules inside the ArcGIS.framework folder. If you installed the SDK in the default location that would be ~/Library/SDKs/ArcGIS/iOS/ArcGIS.framework/Modules. Note that you'll need to create the Modules folder.

 

You'll only need to create that file once and it will work for any Swift projects you create going forward.

 

Create a Swift Project

Start Xcode (I'm currently using Beta-4) and create a new iOS Application project. Choose the Single View Application.

 

In the project settings, add these values so that Xcode can find the ArcGIS SDK:

 

Other Linker Flags: -framework ArcGIS -ObjC -l c++

Framework Search Paths: ~/Library/SDKs/ArcGIS/iOS

 

Add a Map View

In the Main.storyboard add a view.

 

Size this view to fill its superview, and then set then add four constraints to pin the new view to the superview.

 

Show the identity inspector and change the class of the new view to be AGSMapView. Now open ViewController.swift in the assistant editor and create an outlet for the map view (Control-drag from the storyboard to the ViewController class.) If you did everything right Xcode should create a var that looks like this:

 

@IBOutlet weak var mapView: AGSMapView!

 

To fix the warning, change the import statement from UIKit to ArcGIS.

 

Now, to display some data in the map view, make the following changes to viewDidLoad():

 

    override func viewDidLoad() {

        super.viewDidLoad()

      

        let layer = AGSTiledMapServiceLayer(URL: NSURL(string: "http://services.arcgisonline.com/arcgis/rest/services/World_Topo_Map/MapServer"))

        mapView.addMapLayer(layer)

 

        let center = AGSPoint.pointWithX(-7822565, y: 5413016, spatialReference: nil)

        mapView.zoomToScale(72112, withCenterPoint:center, animated: true)

    }

 

 

The last step is to build and run your app. That's it. You're living the Swift lifestyle now.