Displaying Web Map Popups
A map is a more than a picture. It conveys information about our surroundings and helps make decisions. But a picture does not tell the whole story. There is a wealth of information hidden behind the image. Popups are a great way to reveal information about features on a map, such as a business, a location, a natural phenomena, or a geopolitical entity.
You can configure popups when authoring web maps on www.ArcGIS.com. A popup is associated with a specific layer in a web map and describes how information about features in that layer should be presented. When configuring popups, the author of a web map can choose which fields to display, give them aliases, specify formatting for numbers and dates, configure charts, so on and so forth.
Learn more about authoring web maps with popups
Displaying information using popups
AGSPopupsContainerViewController is the main class developers need to use to display popups in their applications. It provides the basic user interface (UI) and behavior for displaying and editing information about features in a popup. It is a container and manages a number of view controllers behind the scenes, each of which is designed for a specific purpose, such as editing an attribute, displaying media, managing attachments, etc. AGSPopupsContainerViewController manages transitions between these view controllers and displays the relevant one depending upon what the user intends to do.
Displaying popups is a 2 step process -
- Instantiate an AGSPopupsContainerViewController
- Display the AGSPopupsContainerViewController
1. Instantiating AGSPopupsContainerViewController
To instantiate AGSPopupsContainerViewController, you need to have a reference to the feature(s) whose information you want to display. The feature(s) could be chosen by a user in a variety of ways - by tapping on the map, by tapping on the accessory button in the callout for a specific feature, or by selecting an option from a table which lists some features.
For popups defined in a web map, you can instantiate an AGSPopupsContainerViewController using initWithWebMap:feature:usingNavigationControllerStack: by passing in a reference to the web map which contains the popup definition, and the feature for which the popup needs to be displayed.
AGSWebMap* webMap = ...;
AGSGraphic* graphic = ...;
AGSPopupsContainerViewController* popupVC = [[AGSPopupsContainerViewController alloc] initWithWebMap:webMap forFeature:feature usingNavigationControllerStack:NO]
If you want to display popups for more than one feature at the same time, you can instantiate an AGSPopupsContainerViewController using initWithPopups:usingNavigationControllerStack: by passing in an array of popups. Each popup is represented by an AGSPopup object which contains references to a feature and the popup definition for that feature.
NSMutableArray* popups = [[[NSMutableArray alloc] init] autorelease];
//The popup definition
AGSPopupInfo* popupInfo = ...;
//The feature to be displayed in a popup
AGSGraphic* graphic = ...;
//Associate the popup definition with the feature to get a popup
AGSPopup* popup = [AGSPopup popupWithGraphic:graphic popupInfo:popupInfo];
[popups addObject:popup];
...
//Pass the list of popups to the view controller
AGSPopupsContainerViewController* popupVC = [[AGSPopupsContainerViewController alloc] initWithPopups:popups usingNavigationControllerStack:false];
A popup definition is represented by an AGSPopupInfo object. You can either create popup definitions programmatically, or retrieve them from a web map.
Retrieving popup definitions from a web map
You can retrieve popup definitions from a web map provided you know which layer or service the popup is associated with. For example, if you have a reference to a graphic belonging to a feature layer, you can retrieve the popup definition for that graphic as so -
AGSGraphic* graphic = ...; //graphic in a feature layer for which we want popup definition
AGSPopupInfo* popupInfo = [self.webmap popupInfoForFeatureLayer:graphic.layer];
There may be times when you cannot easily discern which layer a graphic belongs to, for instance, when the graphic is a result of a query. In such cases, the AGSGraphic object's layer property will be nil and you will need to manually keep track of which layer in the web map contains a popup definition for that feature. In such cases, you can use the URL of the service to retrieve the popup definition from a web map.
NSURL* serviceURL = ...; //the service that the graphic belongs to
int layerId = ...; //sub-layer in the service containing the popup definition
AGSPopupInfo* popupInfo = [self.webmap popupInfoForMapServiceLayerWithURL:serviceURL sublayerId:layerId];
Creating popup definitions programmatically
If you are directly consuming services in your application and not using a web map, or if your web map does not contain any popup definitions, you can create popup definitions programmatically in order to display and/or edit information about features using popups. A popup definition is represented by an AGSPopupInfo object and contains information such as -
- Whether a user should be allowed to edit or delete the feature
- Which attributes of the feature should the popup display, whether they are editable, how to format numbers and dates, etc
- Whether the popup should show attachments for the feature
- What media, such as charts and images, should be displayed for the feature
You can instantiate a new AGSPopupInfo object using the convenience constructor popupInfoForGraphic: by passing in an AGSGraphic object. If the graphic belongs to a feature layer (i.e. the AGSGraphic object's layer property points to a feature layer), the constructor will consult the metadata on the feature layer and create a popup definition with appropriate defaults for the information listed above. Otherwise, it will inspect the attributes of the graphic to create a basic popup definition which you can then tweak.
2. Displaying AGSPopupsContainerViewController
AGSPopupsContainerViewController is a subclass of UIViewController. Like any Cocoa Touch view controller, it can be displayed in a variety of ways depending upon the device being targeted. A full discussion of the ways in which you can display view controllers is outside the scope of this topic and you may refer to Apple's View Controller Programming Guide for more information. Following are some common ways in which you may display an AGSPopupsContainerViewController.
iPhone/iPod Touch
Given the limited screen size on iPhone and iPod Touch devices, the recommended way to display AGSPopupsContainerViewController on such devices is modally - covering the entire screen and obscuring contents beneath it.
popupVC.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:popupVC animated:YES];
For more information on best practices for displaying views modally, refer to the Modal View section of iOS Human Interface Guidelines.
iPad
You have more options for displaying popups on an iPad. You can continue to display AGSPopupsContainerViewController modally, however, you would typically assign it a specific modalPresentationStyle to display it, for example, as a form sheet or page sheet.
popupVC.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
popupVC.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentModalViewController:popupVC animated:YES];
You can even embed AGSPopupsContainerViewController's view in other views such as a map's callout, a popover, a split pane, or some other view in your application.
AGSPopupsContainerViewController* popupVC = ...;
//showing popups in a custom callout
popupVC.view.frame = CGRectMake(0,0,192,288);
popupVC.actionSheetContainerView = self.view;
popupVC.modalPresenter = self;
self.mapView.callout.customView = popupVC.view;
[self.mapView showCalloutAtPoint:location];
//showing popups in a popover
UIPopoverController* popover = [[UIPopoverController alloc]initWithContentViewController:popupVC.view];
[popover setPopoverContentSize:CGSizeMake(320, 480)];
[popover presentPopoverFromRect:location inView:self.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
Note, when you do so, only the initial popup view will be displayed within that view. Subsequent popup views will still be displayed modally, covering the entire screen. This may or may not be consistent with your application's user experience. If you want subsequent views to be displayed differently, for example, within the same view or inside another view, you need to implement the popupsContainer:wantsToShowViewController:ofType:fromViewController:atRect: and popupsContainer:wantsToHideViewController:ofType: delegate methods and take the responsibility of transitioning from one popup view to another.
The following code snippets demonstrate how to display subsequent popup views in a form sheet on an iPad.
- (void) popupsContainer:(id<AGSPopupsContainer>)popupsContainer wantsToShowViewController:(UIViewController*)svc ofType:(AGSPopupViewType)viewType fromViewController:(UIViewController*) fvc atRect:(CGRect) rect {
if(viewType == AGSPopupViewTypeUIImagePicker){
UIPopoverController* popover = [[UIPopoverController alloc]initWithContentViewController:svc];
[popover setPopoverContentSize:CGSizeMake(200, 300)];
[popover presentPopoverFromRect:rect inView:fvc.view permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}else {
svc.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
svc.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentModalViewController:svc animated:YES];
}
}
- (void) popupsContainer: (id< AGSPopupsContainer >)popupsContainer wantsToHideViewController:(UIViewController *) vc ofType:(AGSPopupViewType) viewType {
[self dismissModalViewControllerAnimated:YES];
}
Handling user interaction
AGSPopupsContainerViewController also handles most of user interaction with the UI, such as bringing up an appropriate keyboard when the user starts editing a numeric attribute, allowing the user to take a picture or browse the photo library for attachments, displaying action sheets, etc.
In many cases, AGSPopupsContainerViewController does not implement any default behavior, but instead, provides hooks for developers to handle user interaction on their own. For example, when a user taps the Done button, the AGSPopupsContainerViewController informs its delegate so that it can appropriately dismiss the popup. To respond to such user interaction, you must set one of your classes as the AGSPopupsContainerViewController's delegate. You do this by making your class (typically the view controller which displays the popups) adopt the AGSPopupsContainerDelegate protocol.
@interface MyViewController : UIViewController <AGSPopupsContainerDelegate> {
...
}
An instance of your class must also be set as the view controller's delegate. This will allow it to invoke methods on your class in response to user interaction.
popupVC.delegate = self;
Finally, your class should implement one or more methods defined in the protocol which pertain to the user interaction you want to handle.
@implementation MyViewController {
- (void)popupsContainerDidFinishViewingPopups:(id) popupsContainer {
[self dismissModalViewControllerAnimated:YES];
}
...
}
Editing information using popups
The AGSPopupsContainerViewController provides a UI that makes it very easy to collect information about a feature from the user. The AGSPopupsContainerViewController informs its delegate of various events as the user attempts to edit a feature.
Editing Attributes
The UI for attribute editing automatically applies rules governing the legal values permitted for a feature's attribute. Such rules include enforcement of coded value domains, range domains, length, data type, whether the attribute is read-only, etc. Information regarding the validation that needs to be applied to each attribute is gathered from the feature layer which the feature belongs to.
Editing Attachments
Popups make it very easy to edit media attachments for a feature. Users can attach photos and videos to features either by choosing an existing item from the device's photos library or by using the device's camera, if one is available. You do not need to write any code to implement this functionality, it is provided by the AGSPopupsContainerViewController.
Editing Geometry
AGSPopupsContainerViewController does not provide any default UI for capturing or editing a feature's geometry. Instead, it informs its delegate via the popupsContainer:readyToEditGraphicGeometry:forPopup: method when a user initiates the geometry editing workflow. The geometry that needs to be edited is passed into the method.
It is your responsibility to implement this method and present an appropriate view which will allow the user to edit an existing feature's geometry, or create a new geometry from scratch. You can use the AGSSketchGraphicsLayer to let a user create or edit geometries interactively.
Learn more about using the Sketch Layer
You must also implement the popupsContainer:wantsNewMutableGeometryForPopup: delegate method for cases when the graphic being editing does not have a geometry, for instance, when the user tries to create a new feature. In this method, you must return a an empty geometry that is mutable. AGSPopupsContainerViewController will pass this geometry back to you in popupsContainer:readyToEditGraphicGeometry:ForPopup: and also track its status so that the Done button in the popups view controller can be appropriately enabled/disabled the depending on whether the sketched geometry is valid or not.
- (AGSGeometry *)popupsContainer:(id) popupsContainer wantsNewMutableGeometryForPopup:(AGSPopup *) popup {
//This method is called only if the user tries to create a new feature, or edit a feature that doesnt contain a geometry
//We return an empty mutable geometry of the type that our feature layer uses
return AGSMutableGeometryFromType( ((AGSFeatureLayer*)popup.graphic.layer).geometryType, self.mapView.spatialReference);
}
-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer readyToEditGeometry:(AGSGeometry*)geometry ForPopup:(AGSPopup*)popup{
//On an iPhone, you will need to dismiss the popup view which was displayed modally
[self dismissModalViewControllerAnimated:YES];
//and present another view that will allow the user to sketch on a map.
...
//On an iPad, you may not need to dismiss the popup view if it can be displayed side-by-side
//with the view that the user will sketch on
//In that view, get a reference to the sketch layer in the map
AGSSketchGraphicsLayer* sketchLyr = ...;
//Activate the sketch layer in prepration for the user to sketch
self.mapView.touchDelegate = sketchLyr;
//Use the geometry passed to us as the starting point for the sketch
sketchLyr.geometry = geometry;
}
Persisting Edits
All edits made to a feature via the popup UI exist only locally on the device. If the user quits your application, or if the device powers down, the edits will be lost. AGSPopupsContainerViewController informs its delegate whenever a user edits or deletes a feature. For example, the popupsContainer:didFinishEditingGraphicForPopup: delegate method is invoked when a user has finished editing a feature, and the popupsContainer:wantsToDeleteGraphicForPopup: method is invoked when the user deletes a feature. It is your responsibility to implement such delegate methods and appropriately commit edits to the server or persist them some other way.
You would typically use an AGSFeatureLayer to commit the edits to an ArcGIS Feature Service. You can use methods on the feature layer to commit edits made to a feature's geometry and attributes. You could also use the feature layer to commit edits made to a feature's attachments, but the AGSAttachmentManager is simpler to use and provides an easy, coarse-grained API that is built on top of the feature layer specifically to manage attachments.
-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer didFinishEditingGraphicForPopup:(AGSPopup*)popup{
AGSFeatureLayer *featureLayer = (AGSFeatureLayer*)popup.graphic.layer;
[featureLayer dataChanged];
// simplify & normalize the geometry associated with the feature in case it was sketched by the user
popup.graphic.geometry = [[AGSGeometryEngine defaultGeometryEngine]simplifyGeometry:popup.graphic.geometry];
popup.graphic.geometry = [[AGSGeometryEngine defaultGeometryEngine]normalizeCentralMeridianOfGeometry:popup.graphic.geometry];
featureLayer.editingDelegate = self;
int oid = [featureLayer objectIdForFeature:popup.graphic];
if (oid > 0){
//feature has a valid objectid, this means it exists on the server
//and we simply update the exisiting feature
[featureLayer updateFeatures:[NSArray arrayWithObject:popup.graphic]];
}
else {
//objectid does not exist, this means we need to add it as a new feature
[featureLayer addFeatures:[NSArray arrayWithObject:popup.graphic]];
}
//we will post attachments below when the updates succeed
}
#pragma mark -
#pragma mark AGSFeatureLayerEditingDelegate
-(void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation *)op didFeatureEditsWithResults:(AGSFeatureLayerEditResults *)editResults{
//if edits pertaining to the feature were successful...
if (editResults.addResults.count > 0 || editResults.updateResults.count > 0){
//...we post edits to the attachments
AGSAttachmentManager *attMgr = [featureLayer attachmentManagerForFeature:popupVC.currentPopup.graphic];
attMgr.delegate = self;
if([attMgr hasLocalEdits])
[attMgr postLocalEditsToServer];
}
}
-(void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation *)op didFailFeatureEditsWithError:(NSError *)error{
NSLog(@"Could not commit edits because: %@", [error localizedDescription]);
//Hide the network activity indicator
...
//Display an alert to the user
...
//Restart editing the popup
[popupVC startEditingCurrentPopup];
}
#pragma mark -
#pragma mark AGSAttachmentManagerDelegate
-(void)attachmentManager:(AGSAttachmentManager *)attachmentManager didPostLocalEditsToServer:(NSArray *)attachmentsPosted{
//loop through all attachments looking for failures
BOOL _anyFailure = NO;
for (AGSAttachment* attachment in attachmentsPosted) {
if(attachment.networkError!=nil || attachment.editResultError!=nil){
_anyFailure = YES;
NSString* reason;
if(attachment.networkError!=nil)
reason = [attachment.networkError localizedDescription];
else if(attachment.editResultError !=nil)
reason = attachment.editResultError.errorDescription;
NSLog(@"Attachment '%@' could not be synced with server because %@",attachment.attachmentInfo.name,reason);
}
}
if(_anyFailure){
//warn user of error
}
}
Customizing the UI
Color
You can change the color of the toolbars in the popup views by modifying the style property on AGSPopupsContainerViewController
AGSPopupsContainerViewController* popupVC = ...;
popupVC.style = AGSPopupsContainerStyleCustomColor;
popupVC.styleColor = [UIColor lightGrayColor];
Paging Style
You can change the paging style by modifying the pagingStyle property on AGSPopupsContainerViewController. You can choose between two styles - page control or toolbar.
In both styles, you swipe to view the next or previous popup. The page control is more suitable if you are only displaying a small number of popups (for instance, up to 10) simultaneously. The toolbar is more appropriate for displaying a larger number of popups together as you can easily jump to the beginning and the end of the list. .
Editing Style
You can change the editing style by modifying the editingStyle property on AGSPopupsContainerViewController. You can choose between two styles - inline geometry editing, or geometry editing tool.
With the geometry editing tool style, the user needs to explicitly begin the act of collecting/modifying a feature's geometry by tapping on the tool. The developer needs to handle this event by appropriately displaying a view that will allow the user to sketch a geometry. Although this style can be used for both iPhone and iPad applications, it is more appropriate for iPhone applications where the popup view covers the entire screen and the user needs to click on the geometry tool to bring up another view where he/she can sketch on a map.
On the other hand, with the inline geometry editing style, a user does not need to explicitly initiate the act of collecting/modifying a feature's geometry. It is assumed that the application is already in a state where the user can begin sketching on the map. This style is more appropriate for iPad applications where the popup may be displayed side-by-side with the map, and thus the user can start sketching at any time.
Apart from changing the editing style, you can also specify, for each individual popup, whether the feature can be edited, which fields are editable, whether its geometry can be modified, if it can be deleted, and whether it should show its attachments. These can be controlled by modifying the relevant properties on AGSPopupInfo
Custom Action
You can replace the right bar button in the top toolbar of the popup view by setting the actionButton property on AGSPopupsContainerViewController. You can perform any action when the button is clicked, such as zooming into the feature being displayed in the popup, or displaying a custom action sheet with further options.
Localizing the UI
The text displayed in the popup views has been externalized into a strings file called Localizable.strings to make localization easy. This file is included in the ArcGIS.bundle file under a language specific ".lproj" sub-directory nested within the "Resources" directory. ArcGIS.bundle is installed under ${HOME}/Library/SDKs/ArcGIS/iOS.sdk/user/local/resources. Translations for the following languages are included by default -
- Arabic
- German
- English
- Spanish
- French
- Italian
- Japanese
- Korean
- Portuguese
- Russian
- Chinese
When displaying the UI, the popup view controllers will automatically pick up the appropriate tranlslations from ArcGIS.bundle depending upon the language setting of the device. For this to happen, however, your project must contain atleast one nib file localized for that language. To localize a nib file in XCode 4, select it in the left pane, and add the desired languages to the Localization section in the info pane on the right.
If the language you're targetting is one of those specified in the list above, you don't need to do anything additional because the translations for those languages are already provided with the SDK. If you are targetting another language, you will also need to provide the translated strings for that language as described below.
To add translations for additional languages, you need to create a new language specific ".lproj" sub-directory in ArcGIS.bundle. The sub-directory's name must be inline with Apple's naming convention using a Language ID and optionally a Locale ID. Finally, you need to add a translated version of Localizable.strings to this sub-directory. This strings file must contain translations for each key specified in the strings files provided with the SDK. the The popup view controllers will then be able automatically pick up your translations based on the language setting of the device.
For more information about localizing string resources, refer to Apple's Internationalization Programming topics and Resource Programming Guide
See also
Feature Layer Editing Sample using popups.