Skip To Content ArcGIS for Developers Sign In Dashboard

Edit (Feature Layer)

Download Sample Application

This sample shows you how to create and edit a feature's attributes and geometry. The feature's pop-up manages the creation and editing of both attributes and attachments and integrates the use of the sketch layer to collect the feature's geometry. The pop-ups (AGSPopup) is stored in the pop-up's container view controller (AGSPopupsContainerViewController).

@property (nonatomic,strong) AGSFeatureLayer *featureLayer;

//create the feature layer using OnDemand mode
self.featureLayer = 
  [AGSFeatureLayer featureServiceLayerWithURL:[NSURL URLWithString:@"http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/SanFrancisco/311Incidents/FeatureServer/0"] 
  mode:AGSFeatureLayerModeOnDemand 
  credential:nil];

//set the editing delegate so that delegates are called when features are edited
self.featureLayer.editingDelegate = self;

//add the layer to the MapView
[self.mapView addMapLayer:self.featureLayer withName:@"Feature Layer"];

For more information on the Pop-up classes please view the sample code in the implementation file.

Sample Code

//SWIFT SAMPLE CODE
/*
Copyright 2014 Esri

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import Cocoa
import ArcGIS

let kFLETiledMapServiceURL  = "http://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"
let kFLEFeatureServiceURL = "http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/SanFrancisco/311Incidents/FeatureServer/0"

class FeatureLayerEditingSwiftSample: NSViewController, AGSMapViewTouchDelegate, AGSMapViewLayerDelegate, AGSLayerDelegate, AGSPopupsContainerDelegate, AGSAttachmentManagerDelegate, AGSFeatureLayerEditingDelegate {
    
    @IBOutlet weak var addFeatureButton:NSButton!
    @IBOutlet weak var mapView:AGSMapView!
    @IBOutlet weak var popupView:NSView!
    @IBOutlet weak var splitView:NSSplitView!
    
    var featureLayer:AGSFeatureLayer!
    var sketchLayer:AGSSketchGraphicsLayer!
    var popupsContainerVC:AGSPopupsContainerViewController!
    var popups = Array<AGSPopup>()
    var mapPoint:AGSPoint!
    var currentPopup:AGSPopup!
    var currentEditingPopup:AGSPopup!
    var originalFeatureAttributes:Dictionary<NSObject, AnyObject>!
    var originalGeometry:AGSGeometry!
    var inEditingMode = false
    
    
    // -------------------------------------------------------------------------------
    //  awakeFromNib
    // -------------------------------------------------------------------------------
    override func awakeFromNib() {
        
        //enable wrap around
        self.mapView.enableWrapAround()
        
        //set mapView delegates
        self.mapView.layerDelegate = self
        self.mapView.touchDelegate = self
        self.mapView.showMagnifierOnTapAndHold = true
        
        //add base layer to map and set delegate to know when layer loads or fails to load
        let baseMapLayer = AGSTiledMapServiceLayer(URL: NSURL(string: kFLETiledMapServiceURL))
        baseMapLayer.delegate = self
        self.mapView.addMapLayer(baseMapLayer, withName:"Base Layer")
        
        //add feature layer to map
        self.featureLayer = AGSFeatureLayer(URL: NSURL(string: kFLEFeatureServiceURL), mode: .OnDemand)
        self.featureLayer.delegate = self
        self.featureLayer.outFields = ["*"]
        self.mapView.addMapLayer(self.featureLayer, withName:"Feature Layer")
        
        //add sketch graphics layer to map
        self.sketchLayer = AGSSketchGraphicsLayer()
        self.mapView.addMapLayer(self.sketchLayer, withName:"Sketch Graphics Layer")
        
        //add sketch graphics layer to map
        self.sketchLayer = AGSSketchGraphicsLayer()
        self.mapView.addMapLayer(self.sketchLayer, withName:"Sketch Graphics Layer")
        
        //zoom to predefined extend with known spatial reference of the map
        let sr = AGSSpatialReference.webMercatorSpatialReference()
        let env = AGSEnvelope(xmin: -13623000, ymin:4547000, xmax:-13613000, ymax:4557000, spatialReference:sr)
        self.mapView.zoomToEnvelope(env, animated:true)
    }

    //MARK: - AGSLayerDelegate Methods
    
    func layer(layer: AGSLayer!, didFailToLoadWithError error: NSError!) {
        if let viewWindow = self.view.window {
            let alert = NSAlert()
            alert.informativeText = error.localizedDescription
            alert.messageText = "Failed to load layer: \(layer.name)"
            alert.alertStyle = .InformationalAlertStyle
            alert.beginSheetModalForWindow(viewWindow, modalDelegate:self, didEndSelector:nil, contextInfo:nil)
        }
    }
    
    //MARK: - AGSMapViewTouchDelegate Methods
    
    func mapView(mapView: AGSMapView!, didClickAtPoint screen: CGPoint, mapPoint mappoint: AGSPoint!, features: [NSObject : AnyObject]!) {
        //If not editing then select an existing feature and display it's popup
        if !self.inEditingMode {
            
            //Clear the PopupsContainer VC and popup view
            self.resetPopupsAndSubviews()
            
            //Check that the selected features belong to the featurelayer
            //Add the PopupInformation from the selected features into the PopupArray
            //A PopupArray is used because you may have selected more than one feature
            self.mapPoint = mappoint
            if features.count > 0 {
                for (layerName, anyObject) in features {
                    if layerName == self.featureLayer.name {
                        for graphic in features[layerName] as [AGSGraphic] {
                            self.addPopupToPopupsForGraphic(graphic)
                        }
                    }
                }
                //Populate the PopupsContainer VC using the PopupArray
                self.populatePopupsContainer()
            }
        }
    }
    
    func mapView(mapView: AGSMapView!, didKeyDown event: NSEvent!) {
        //delete key is keyCode = 51
        if (event.keyCode == 51){
            self.sketchLayer.removeSelectedVertex()
        }
    }
    
    //MARK: - AGSPopupsContainerDelegate Methods
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, readyToEditGraphicGeometry geometry: AGSGeometry!, forPopup popup: AGSPopup!) {
        
        //register self for receiving notifications from the sketch layer
        NSNotificationCenter.defaultCenter().addObserver(self, selector: "respondToGeomChanged:", name: AGSSketchGraphicsLayerGeometryDidChangeNotification, object: nil)
        
        // set toch delegate to sketch layer
        self.mapView.touchDelegate = self.sketchLayer
        
        //This is the starting point of the sketch
        self.sketchLayer.geometry = geometry
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, didStartEditingForPopup popup: AGSPopup!) {
        
        // store original attribute and geometry
        // so can revert back if editing is canceled
        self.originalFeatureAttributes = popup.feature.allAttributes()
        if popup.feature.geometry != nil {
            self.originalGeometry = popup.feature.geometry.mutableCopy() as AGSGeometry
        }
        
        // set in editing
        self.inEditingMode = true
        
        // disable button
        self.addFeatureButton.enabled = false
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, didFinishEditingForPopup popup: AGSPopup!) {
        
        // finish editing
        self.finishEditingPopup(popup)
        
        // set in editing
        self.inEditingMode = false
        
        // enable buttons
        self.addFeatureButton.enabled = true
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, didChangeToCurrentPopup popup: AGSPopup!) {
        // change current popup
        // set selection
        self.currentPopup = popup
        self.refreshSelection()
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, wantsToDeleteForPopup popup: AGSPopup!) {
        if let viewWindow = self.view.window {
            let alert = NSAlert()
            alert.addButtonWithTitle("Delete")
            alert.addButtonWithTitle("Cancel")
            alert.informativeText = "This operation cannot be undone."
            alert.messageText = "Are you sure you want to delete this feature?"
            alert.alertStyle = .WarningAlertStyle
            
            alert.beginSheetModalForWindow(viewWindow, modalDelegate:self, didEndSelector:"deleteFeatureErrorAlertDidEnd:returnCode:", contextInfo:nil)
        }
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, didCancelEditingForPopup popup: AGSPopup!) {
        
        self.currentEditingPopup = popup
        self.cancelEditing()
        
        // set in editing
        self.inEditingMode = false
        
        // enable buttons
        self.addFeatureButton.enabled = true
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, wantsNewMutableGeometryForPopup popup: AGSPopup!) -> AGSGeometry! {
        //Return an empty mutable geometry of the type that our feature layer uses
        return AGSMutableGeometryFromType((popup.graphic.layer as AGSFeatureLayer).geometryType, self.mapView.spatialReference)
    }
    
    //MARK: - AGSFeatureLayerEditingDelegate methods
    
    func featureLayer(featureLayer: AGSFeatureLayer!, operation op: NSOperation!, didFeatureEditsWithResults editResults: AGSFeatureLayerEditResults!) {
        
        //We will assume we have to update the attachments unless
        //1) We were adding a feature and it failed
        //2) We were updating a feature and it failed
        //3) We were deleting a feature
        var updateAttachments = true
        
        var errorMessage = String()
        
        if editResults.addResults != nil && editResults.addResults.count > 0 {
            //we were adding a new feature
            let result = editResults.addResults[0] as AGSEditResult
            if !result.success {
                //Add operation failed. We will not update attachments
                updateAttachments = false
                //Inform user
                errorMessage = result.error.description
                self.warnUserOfErrorWithMessage("Could not add feature. Please try again")
            }
        }
        else if editResults.updateResults != nil && editResults.updateResults.count > 0 {
            //we were updating a feature
            let result = editResults.updateResults[0] as AGSEditResult
            if !result.success {
                //Update operation failed. We will not update attachments
                updateAttachments = false
                //Inform user
                errorMessage = result.error.description
                self.warnUserOfErrorWithMessage("Could not update feature. Please try again")
            }
        }
        else if editResults.deleteResults != nil && editResults.deleteResults.count > 0 {
            //we were deleting a feature
            updateAttachments = false
            let result = editResults.deleteResults[0] as AGSEditResult
            if !result.success {
                //Delete operation failed. Inform user
                errorMessage = result.error.description
                self.warnUserOfErrorWithMessage("Could not delete feature. Please try again")
            }
            else {
                //Delete operation succeeded
                //Dismiss the popup view controller and hide the callout which may have been shown for
                //the deleted feature.
                self.mapView.callout.hidden = true
                self.popupsContainerVC = nil
            }
        }
        
        if !errorMessage.isEmpty {
            self.warnUserOfErrorWithMessage(errorMessage)
        }
        
        //if edits pertaining to the feature were successful...
        if updateAttachments {
            
            //...we post edits to the attachments
            let attMgr = featureLayer.attachmentManagerForFeature(self.currentPopup.graphic)
            attMgr.delegate = self
            
            if attMgr.hasLocalEdits() {
                attMgr.postLocalEditsToServer()
            }
            else {
                self.currentEditingPopup.featureLayer.refresh()
                self.currentPopup = self.popups[0]
                self.refreshSelection()
            }
        }
        
        self.sketchLayer.clear()
    }
    
    func featureLayer(featureLayer: AGSFeatureLayer!, operation op: NSOperation!, didFailFeatureEditsWithError error: NSError!) {
        println("Could not commit edits because: \(error.localizedDescription)")
        
        self.warnUserOfErrorWithMessage("Could not save edits. Please try again")
    }
    
    //MARK: - AGSAttachmentManagerDelegate
    
    func attachmentManager(attachmentManager: AGSAttachmentManager!, didPostLocalEditsToServer attachmentsPosted: [AnyObject]!) {
        
        //loop through all attachments looking for failures
        var anyFailure = false
        
        for attachment in attachmentsPosted as [AGSAttachment] {
            if attachment.networkError != nil || attachment.editResultError != nil {
                anyFailure = true
                var reason:String!
                if attachment.networkError != nil {
                    reason = attachment.networkError.localizedDescription
                }
                else if attachment.editResultError != nil {
                    reason = attachment.editResultError.errorDescription
                }
                println("Attachment \(attachment.attachmentInfo.name) could not be synced with server because \(reason)")
            }
        }
        
        if anyFailure {
            self.warnUserOfErrorWithMessage("Some attachment edits could not be synced with the server. Please try again")
        }
        else {
            //no errors, so refresh feature layer
            self.currentEditingPopup.featureLayer.refresh()
            self.currentPopup = self.popups[0]
            self.refreshSelection()
        }
    }

    //MARK: - Helper methods
    
    @IBAction func addFeature(sender:AnyObject) {
        
        if self.featureLayer != nil {
            
            var graphic:AGSGraphic!
            
            if self.featureLayer.types.count != 0 {
                let featureType = (self.featureLayer.types)[0] as AGSFeatureType
                graphic = self.featureLayer.featureWithType(featureType)
            }
            else if self.featureLayer.templates.count != 0 {
                let featureTemplate = (self.featureLayer.templates)[0] as AGSFeatureTemplate
                graphic = self.featureLayer.featureWithTemplate(featureTemplate)
            }
            else {
                println("No Feature Type/Template")
            }
            
            self.addFeatureButton.enabled = false
            self.featureLayer.addGraphic(graphic)
            self.resetPopupsAndSubviews()
            self.addPopupToPopupsForGraphic(graphic)
            self.populatePopupsContainer()
            self.popupsContainerVC.startEditingCurrentPopup()
            self.mapView.callout.hidden = true
        }
    }
    
    func addPopupToPopupsForGraphic(graphic:AGSGraphic) {
        // add popup to popups array
        self.popupView.subviews = Array()
        let popupInfo = AGSPopupInfo(forGraphic: graphic)
        popupInfo.title = self.featureLayer.serviceLayerName
        
        let popup = AGSPopup(graphic: graphic, popupInfo:popupInfo)
        
        popup.allowEdit = true
        popup.allowEditGeometry = true
        popup.allowDelete = true
        
        self.popups.append(popup)
    }
    
    func cancelEditing() {
        
        if self.currentEditingPopup != nil {
            self.currentEditingPopup.graphic.setAttributes(self.originalFeatureAttributes)
            self.currentEditingPopup.feature.geometry = self.originalGeometry
            self.currentEditingPopup.featureLayer.refresh()
            
            self.currentPopup = self.popups[0]
            self.currentEditingPopup = nil
        }
        self.sketchLayer.clear()
        self.mapView.touchDelegate = self
    }
    
    func deleteFeatureErrorAlertDidEnd(alert:NSAlert, returnCode:(Int)) {
        alert.window.orderOut(nil)
        
        if returnCode == NSAlertFirstButtonReturn {
            
            //delete the feature...
            let activeFeatureLayer = self.currentPopup.graphic.layer as AGSFeatureLayer
            let number = NSNumber(longLong: activeFeatureLayer.objectIdForFeature(self.currentPopup.graphic))
            let oids = [number]
            
            //Call method on feature layer to delete the feature
            self.featureLayer.deleteFeaturesWithObjectIds(oids)
            self.featureLayer.refresh()
            self.mapView.callout.hidden = true
            self.resetPopupsAndSubviews()
            
        }
    }
    
    
    func featureEditingErrorAlertDidEnd(alert:NSAlert, returnCode:(Int)) {
        alert.window.orderOut(nil)
        
        if returnCode == NSAlertFirstButtonReturn {
            
            //try again to post edits
            self.finishEditingPopup(self.currentEditingPopup)
        }
        else if returnCode == NSAlertSecondButtonReturn {
            
            //cancel editing/posting edits
            self.cancelEditing()
            
            //This is so the popup VC knows something happened to cancel the editing process
            //and redraw itself and the current popup vc accordingly
            self.popupsContainerVC.cancelEditingCurrentPopup()
        }
    }
    
    func finishEditingPopup(popup: AGSPopup) {
    
        self.currentEditingPopup = popup
        
        //set the feature layer editing delegate to ourself
        popup.featureLayer.editingDelegate = self
        
        // simplify the geometry, this will take care of self intersecting polygons. Required for polygon geometries
        //popup.graphic.geometry = [[AGSGeometryEngine defaultGeometryEngine]simplifyGeometry:popup.graphic.geometry];
        
        //normalize the geometry, this will take care of geometries that extend beyone the dateline. Required for polygon or linear geometries
        //(if wraparound was enabled on the map)
        //popup.graphic.geometry = [[AGSGeometryEngine defaultGeometryEngine]normalizeCentralMeridianOfGeometry:popup.graphic.geometry];
        
        let activeFeatureLayer = popup.graphic.layer as AGSFeatureLayer
        let oid = activeFeatureLayer.objectIdForFeature(popup.graphic)
        
        //'SAVE THE ATTACHMENTS'
        
        if oid > 0 {
            //feature has a valid objectid, this means it exists on the server
            //and we simply update the exisiting feature
            activeFeatureLayer.updateFeatures([popup.graphic])
            
        } else {
            //objectid does not exist, this means we need to add it as a new feature
            activeFeatureLayer.addFeatures([popup.graphic])
        }
        
        self.currentPopup = popup
        self.mapView.touchDelegate = self
        self.sketchLayer.geometry = nil
        self.sketchLayer.clear()
        
        self.inEditingMode = false
        
        self.refreshSelection()
        self.addFeatureButton.enabled = true
    
    }
    
    func refreshSelection() {
    
        self.featureLayer.clearSelection()
        self.featureLayer.setSelected(true, forGraphic:self.currentPopup.graphic)
    
    }
    
    func populatePopupsContainer() {
    
        if self.popupsContainerVC == nil || self.popupsContainerVC.popups.count == 0 {
            //Initialize the PopupsContainer VC with first layer's popups
            self.popupsContainerVC = AGSPopupsContainerViewController(popups: self.popups)
            //select the graphic for the first popup in the array
            self.currentPopup = self.popups[0]
            self.refreshSelection()
        }
        else {
            //The popupsContainerVC has already been initialized, Hence add additional popups from other layers.
            self.popupsContainerVC.showAdditionalPopups(self.popups)
        }
        
        if self.popupView.frame.size.width < 100.0  {
            self.splitView.setPosition(100.0, ofDividerAtIndex:0)
        }
        self.popupsContainerVC.view.frame = self.popupView.bounds
        self.popupsContainerVC.delegate = self
        
        //Add the PopupsContainer VC to the popup view
        self.popupView.addSubview(self.popupsContainerVC.view)
        self.popupsContainerVC.view.autoresizingMask = .ViewWidthSizable | .ViewHeightSizable;
    }
    
    func resetPopupsAndSubviews() {
        //Initialize Popups Array; If it already exists, remove all it's objects and reset it
        if self.popups.count > 0 {
            self.popups.removeAll(keepCapacity: false)
        }
        else {
            self.popups = Array()
        }
        //Clear the popups from the popupsContainerVC so that the latter can be reused.
        if self.popupsContainerVC != nil {
            self.popupsContainerVC.clearAllPopups()
        }
        //Reset the subview of the popupView
        if self.popupView.subviews.count > 0  {
            self.popupView.subviews = Array()
        }
        
        self.currentPopup = nil
        self.currentEditingPopup = nil
        
        self.splitView.setPosition(0.0, ofDividerAtIndex:0)
    }
    
    func respondToGeomChanged(notification: NSNotification) {
        if self.popupsContainerVC != nil {
            self.popupsContainerVC.geometryUpdated()
        }
    }
    
    func warnUserOfErrorWithMessage(message: String) {
    //Display an alert to the user
        if let viewWindow = self.view.window {
            let alert = NSAlert()
            alert.addButtonWithTitle("Try Again")
            alert.addButtonWithTitle("Cancel")
            alert.messageText = "Error"
            alert.informativeText = message
            alert.beginSheetModalForWindow(viewWindow, modalDelegate:self, didEndSelector:"featureEditingErrorAlertDidEnd:returnCode:", contextInfo:nil)
        }
    }

    //MARK: - NSSplitView delegate
    
    // -------------------------------------------------------------------------------
    //  splitView:constrainMaxCoordinate
    //
    //  this will restrict resize of splitView at specified max value
    // -------------------------------------------------------------------------------
    func splitView(splitView: NSSplitView, constrainMaxCoordinate proposedMaximumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat {
        return 350
    }
    
    // -------------------------------------------------------------------------------
    //  splitView:constrainMinCoordinate
    //
    //  this will restrict resize of splitView at specified min value
    // -------------------------------------------------------------------------------
    func splitView(splitView: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt dividerIndex: Int) -> CGFloat {
        return 250
    }
    
    //MARK: -
    
    deinit {
        //remove observer
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
    
}
//OBJECTIVE C SAMPLE CODE
/*
 Copyright 2013 Esri
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
 http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

#import "FeatureLayerEditingSample.h"

#define kTiledMapServiceURL @"http://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"
#define kFeatureServiceURL @"http://sampleserver3.arcgisonline.com/ArcGIS/rest/services/SanFrancisco/311Incidents/FeatureServer/0"

@interface FeatureLayerEditingSample ()

@property (nonatomic,strong) AGSFeatureLayer *featureLayer;
@property (nonatomic,strong) AGSSketchGraphicsLayer *sketchLayer;
@property (nonatomic,strong) AGSPopupsContainerViewController *popupsContainerVC;
@property (nonatomic,strong) NSMutableArray *popups;
@property (nonatomic,strong) AGSPoint *mapPoint;
@property (nonatomic,strong) AGSPopup *currentPopup;
@property (nonatomic,strong) AGSPopup *currentEditingPopup;
@property (nonatomic,strong) NSDictionary *originalFeatureAttributes;
@property (nonatomic,strong) AGSGeometry *originalGeometry;
@property (nonatomic,assign) BOOL inEditingMode;

@end

@implementation FeatureLayerEditingSample


// -------------------------------------------------------------------------------
//  awakeFromNib
// -------------------------------------------------------------------------------
- (void)awakeFromNib {
    
    //enable wrap around
    [self.mapView enableWrapAround];
    
    //set layer delegate to know when mapView loads
    self.mapView.layerDelegate = self;
    self.mapView.touchDelegate = self;
    self.mapView.showMagnifierOnTapAndHold = YES;
    
    //add base layer to map and set delegate to know when layer loads or fails to load
	AGSTiledMapServiceLayer *baseMapLayer = [[AGSTiledMapServiceLayer alloc] initWithURL:[NSURL URLWithString:kTiledMapServiceURL]];
    baseMapLayer.delegate = self;
	[self.mapView addMapLayer:baseMapLayer withName:@"Base Map"];
    
    //add feature layer
    self.featureLayer = [AGSFeatureLayer featureServiceLayerWithURL:[NSURL URLWithString:kFeatureServiceURL] mode:AGSFeatureLayerModeOnDemand credential:nil];
    self.featureLayer.delegate = self;
    self.featureLayer.outFields = @[@"*"];
    self.featureLayer.bufferFactor = 0;
    self.featureLayer.editingDelegate = self;
    [self.mapView addMapLayer:self.featureLayer withName:@"Feature Layer"];
    
    //add sketch graphics layer to map
    self.sketchLayer = [[AGSSketchGraphicsLayer alloc]init];
    [self.mapView addMapLayer:self.sketchLayer withName:@"Sketch Graphics Layer"];
    
    //zoom to predefined extend with known spatial reference of the map
	AGSSpatialReference *sr = [AGSSpatialReference webMercatorSpatialReference];
	AGSEnvelope *env = [AGSEnvelope envelopeWithXmin:-13623000 ymin:4547000 xmax:-13613000 ymax:4557000 spatialReference:sr];
	[self.mapView zoomToEnvelope:env animated:YES];
    
    self.inEditingMode = NO;
}


#pragma mark - AGSLayerDelegate Methods

- (void)layer:(AGSLayer *)layer didFailToLoadWithError:(NSError *)error {
	NSString *layerNameStr = [NSString stringWithFormat:@"Failed to Load Layer! %@",layer.name];
	NSAlert *alert = [[NSAlert alloc] init];
    [alert setAlertStyle:NSInformationalAlertStyle];
    [alert setMessageText:layerNameStr];
    [alert setInformativeText:[error localizedDescription]];
	[alert beginSheetModalForWindow:self.view.window modalDelegate:self didEndSelector:nil contextInfo:nil];
}


#pragma mark - AGSMapViewTouchDelegate Methods

- (void)mapView:(AGSMapView *)mapView didClickAtPoint:(CGPoint)screen mapPoint:(AGSPoint *)mappoint features:(NSDictionary *)features {
    
    //If not editing then select an existing feature and display it's popup
    if (!self.inEditingMode) {

        //Clear the PopupsContainer VC and popup view
        [self resetPopupsAndSubviews];
        
        //Check that the selected features belong to the featurelayer
        //Add the PopupInformation from the selected features into the PopupArray
        //A PopupArray is used because you may have selected more than one feature
        self.mapPoint = mappoint;
        if ([features count] > 0) {
            for (NSString *layerName in features) {
                if ([layerName isEqual: self.featureLayer.name]){
                    for (AGSGraphic *graphic in [features valueForKey:layerName]) {
                        [self addPopupToPopupsForGraphic:graphic];
                    }
                }
            }
            //Populate the PopupsContainer VC using the PopupArray
            [self populatePopupsContainer];
        }
    }
}

-(void)mapView:(AGSMapView *)mapView didKeyDown:(NSEvent *)event{
    //delete key is keyCode = 51
    if (event.keyCode == 51){
        [self.sketchLayer removeSelectedVertex];
    }
}


#pragma mark - AGSPopupsContainerDelegate Methods

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer readyToEditGeometry:(AGSGeometry*)geometry forPopup:(AGSPopup*)popup {
    
    //register self for receiving notifications from the sketch layer
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(respondToGeomChanged:) name:AGSSketchGraphicsLayerGeometryDidChangeNotification object:nil];
    
    self.mapView.touchDelegate = self.sketchLayer;
    
    //This is the starting point of the sketch
    self.sketchLayer.geometry = geometry;
}

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer didStartEditingForPopup:(AGSPopup*)popup {
    
    self.originalFeatureAttributes = [popup.graphic allAttributes];
    self.originalGeometry = [popup.graphic.geometry mutableCopy];
    
    self.inEditingMode = YES;
    [self.addFeatureButton setEnabled:FALSE];
}

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer didFinishEditingForPopup:(AGSPopup*)popup {
    
    [self finishEditingPopup:popup];
    self.inEditingMode = NO;
    [self.addFeatureButton setEnabled:TRUE];
}

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer didChangeToCurrentPopup:(AGSPopup *)popup{
    self.currentPopup = popup;
    [self refreshSelection];
}

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer wantsToDeleteForPopup:(AGSPopup *)popup{
    
    NSAlert *alert = [[NSAlert alloc] init];
    [alert addButtonWithTitle:@"Delete"];
    [alert addButtonWithTitle:@"Cancel"];
    [alert setInformativeText:@"This operation cannot be undone."];
    [alert setMessageText:@"Are you sure you want to delete this feature?"];
    [alert setAlertStyle:NSWarningAlertStyle];
    
    [alert beginSheetModalForWindow:self.view.window modalDelegate:self didEndSelector:@selector(deleteFeatureErrorAlertDidEnd:returnCode:contextInfo:) contextInfo:nil];
}

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer didCancelEditingForPopup:(AGSPopup*)popup{
    
    self.currentEditingPopup = popup;
    [self cancelEditing];
    
    // set in editing
    self.inEditingMode = NO;
    
    // enable buttons
    self.addFeatureButton.enabled = YES;
    
}


-(AGSGeometry*)popupsContainer:(id<AGSPopupsContainer>)popupsContainer wantsNewMutableGeometryForPopup:(AGSPopup*)popup {
    
    //Return an empty mutable geometry of the type that our feature layer uses
    return AGSMutableGeometryFromType(((AGSFeatureLayer*)popup.graphic.layer).geometryType, self.mapView.spatialReference);
}

#pragma mark - FeatureLayerEditingDelegate Methods

-(void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation *)op didFailFeatureEditsWithError:(NSError *)error {
    
    NSString *errorMessage = [NSString stringWithFormat:@"%@: %@", NSLocalizedString(@"An error occurred while posting feature edits",nil),[error localizedDescription]];
    [self warnUserOfErrorWithMessage:errorMessage];
}

- (void)featureLayer:(AGSFeatureLayer *)featureLayer operation:(NSOperation*)op didFeatureEditsWithResults:(AGSFeatureLayerEditResults *)editResults {
    
    //We will assume we have to update the attachments unless
    //1) We were adding a feature and it failed
    //2) We were updating a feature and it failed
    //3) We were deleting a feature
    BOOL updateAttachments = YES;
    
    NSString *errorMessage = nil;
    if ([editResults.addResults count] > 0) {
        //Adding a new feature
        AGSEditResult* result = (AGSEditResult*)[editResults.addResults objectAtIndex:0];
        if (!result.success) {
            //Add operation failed. We will not update attachments
            updateAttachments = NO;
            //Inform user
            errorMessage = [result.error errorDescription];
            [self warnUserOfErrorWithMessage:errorMessage];
        }
        
    }else if ([editResults.updateResults count] > 0) {
        //we were updating a feature
        AGSEditResult* result = (AGSEditResult*)[editResults.updateResults objectAtIndex:0];
        if(!result.success){
            //Update operation failed. We will not update attachments
            updateAttachments = NO;
            //Inform user
            errorMessage = [result.error errorDescription];
            [self warnUserOfErrorWithMessage:@"Could not update feature. Please try again"];
        }
    }else if ([editResults.deleteResults count] > 0) {
        //we were deleting a feature
        updateAttachments = NO;
        AGSEditResult* result = (AGSEditResult*)[editResults.deleteResults objectAtIndex:0];
        if(!result.success){
            //Delete operation failed. Inform user
            errorMessage = [result.error errorDescription];
            [self warnUserOfErrorWithMessage:@"Could not delete feature. Please try again"];
        }
        else {
            //Delete operation succeeded
            //Dismiss the popup view controller and hide the callout which may have been shown for
            //the deleted feature.
            self.mapView.callout.hidden = YES;
            self.popupsContainerVC = nil;
        }
    }
    if ([errorMessage length] > 0) {
        [self warnUserOfErrorWithMessage:errorMessage];
    }
    
    if (updateAttachments) {
        
        //...we post edits to the attachments
		AGSAttachmentManager *attMgr = [featureLayer attachmentManagerForFeature:self.currentPopup.graphic];
		attMgr.delegate = self;
        
        if ([attMgr hasLocalEdits]) {
			[attMgr postLocalEditsToServer];
        }
        else {
            //no local edits, so refresh feature layer here
            [self.currentEditingPopup.featureLayer refresh];
            self.currentPopup = [self.popups objectAtIndex:0];
            [self refreshSelection];
        }
	}
    
    [self.sketchLayer clear];
    
}

#pragma mark - AGSAttachmentManagerDelegate Methods
-(void)attachmentManager:(AGSAttachmentManager *)attachmentManager didPostLocalEditsToServer:(NSArray *)attachmentsPosted{
    
    //loop through all attachments looking for failures
    NSInteger numFailed = 0;
	NSInteger numAttachmentEdits = 0;
    for (AGSAttachment* attachment in attachmentsPosted) {
        if (attachment.networkError != nil || attachment.editResultError != nil) {
            numFailed++;
            NSString* reason = nil;
            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);
        }
        
        numAttachmentEdits++;
    }
    
    if (numFailed > 0){
        NSString *errorMessage = [NSString stringWithFormat:NSLocalizedString(@"%d of %d attachment edits failed to post.", nil), numFailed, numAttachmentEdits];
        [self warnUserOfErrorWithMessage:errorMessage];
    }
    else {
        //no errors, so refresh feature layer
        [self.currentEditingPopup.featureLayer refresh];
        self.currentPopup = [self.popups objectAtIndex:0];
        [self refreshSelection];
    }
}

#pragma mark - Helper Methods

- (void)addFeature:(id)sender {
	
    if (self.featureLayer) {
        
		AGSGraphic *graphic = nil;
		
		if ([self.featureLayer.types count] != 0) {
			AGSFeatureType *featureType = (self.featureLayer.types)[0];
			graphic = [self.featureLayer featureWithType:featureType];
		}
		else if ([self.featureLayer.templates count] != 0) {
			AGSFeatureTemplate *featureTemplate = (self.featureLayer.templates)[0];
			graphic = [self.featureLayer featureWithTemplate:featureTemplate];
		}
		else {
            NSLog(@"No Feature Type/Template");
		}
		
        [self.addFeatureButton setEnabled:FALSE];
		[self.featureLayer addGraphic:graphic];
        [self resetPopupsAndSubviews];
		[self addPopupToPopupsForGraphic:graphic];
		[self populatePopupsContainer];
        [self.popupsContainerVC startEditingCurrentPopup];
        self.mapView.callout.hidden = YES;
	}
}

- (void)addPopupToPopupsForGraphic:(AGSGraphic *)graphic {
    
    //Add the PopupInformation from the selected features into the PopupArray
	[self.popupView setSubviews:[NSArray array]];
	AGSPopupInfo *popupInfo = [AGSPopupInfo popupInfoForGraphic:graphic];
	popupInfo.title = self.featureLayer.serviceLayerName;
	
	AGSPopup *popup = [AGSPopup popupWithGraphic:graphic popupInfo:popupInfo];
    
    popup.allowEdit = YES;
	popup.allowEditGeometry = YES;
	popup.allowDelete = YES;
    
	[self.popups addObject:popup];
}


-(void)cancelEditing {
    
    [self.currentEditingPopup.graphic setAttributes:self.originalFeatureAttributes];
    self.currentEditingPopup.graphic.geometry = self.originalGeometry;
    [self.currentEditingPopup.featureLayer refresh];
    
    self.currentPopup = [self.popups objectAtIndex:0];
    [self refreshSelection];
    
    self.currentEditingPopup = nil;
    [self.sketchLayer clear];
    self.mapView.touchDelegate = self;

    
}

-(void)deleteFeatureErrorAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo{
    
    [[alert window]orderOut:nil];
    
    if (returnCode == NSAlertFirstButtonReturn){
        
        //delete the feature...
        AGSFeatureLayer *activeFeatureLayer = (AGSFeatureLayer*)self.currentPopup.graphic.layer;
        NSNumber* number = [NSNumber numberWithInteger: [activeFeatureLayer objectIdForFeature:self.currentPopup.graphic]];
        NSArray* oids = [NSArray arrayWithObject: number];
        
        //Call method on feature layer to delete the feature
        [self.featureLayer deleteFeaturesWithObjectIds:oids];
        [self.featureLayer refresh];
        self.mapView.callout.hidden = YES;
        [self resetPopupsAndSubviews];
        
        
    }
}

-(void)featureEditingErrorAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo{
    
    [[alert window]orderOut:nil];
    
    if (returnCode == NSAlertFirstButtonReturn){
        
        //try again to post edits
        [self finishEditingPopup:self.currentEditingPopup];
    }
    else if (returnCode == NSAlertSecondButtonReturn){
        
        //cancel editing/posting edits
        [self cancelEditing];
        
        //This is so the popup VC knows something happened to cancel the editing process
        //and redraw itself and the current popup vc accordingly
        [self.popupsContainerVC cancelEditingCurrentPopup];
    }
}

- (void)finishEditingPopup:(AGSPopup *)popup {
    
    self.currentEditingPopup = popup;
    
    //set the feature layer editing delegate to ourself
    popup.featureLayer.editingDelegate = self;
    
    // simplify the geometry, this will take care of self intersecting polygons. Required for polygon geometries
    //popup.graphic.geometry = [[AGSGeometryEngine defaultGeometryEngine]simplifyGeometry:popup.graphic.geometry];
    
    //normalize the geometry, this will take care of geometries that extend beyone the dateline. Required for polygon or linear geometries
    //(if wraparound was enabled on the map)
    //popup.graphic.geometry = [[AGSGeometryEngine defaultGeometryEngine]normalizeCentralMeridianOfGeometry:popup.graphic.geometry];
	
    AGSFeatureLayer *activeFeatureLayer = (AGSFeatureLayer*)popup.graphic.layer;
	long oid = [activeFeatureLayer objectIdForFeature:popup.graphic];
	
    //'SAVE THE ATTACHMENTS'
    
	if (oid > 0){
		//feature has a valid objectid, this means it exists on the server
        //and we simply update the exisiting feature
		[activeFeatureLayer updateFeatures:[NSArray arrayWithObject:popup.graphic]];
        
	} else {
		//objectid does not exist, this means we need to add it as a new feature
		[activeFeatureLayer addFeatures:[NSArray arrayWithObject:popup.graphic]];
	}

    self.currentPopup = popup;
    self.mapView.touchDelegate = self;
    self.sketchLayer.geometry = nil;
    [self.sketchLayer clear];
    
    self.inEditingMode = NO;

    [self refreshSelection];
    [self.addFeatureButton setEnabled:TRUE];
    
}

-(void)refreshSelection {
    
    [self.featureLayer clearSelection];
    [self.featureLayer setSelected:YES forGraphic:self.currentPopup.graphic];
    
}

-(void) populatePopupsContainer {
    
	if (!self.popupsContainerVC || ([self.popupsContainerVC.popups count] == 0)) {
        //Initialize the PopupsContainer VC with first layer's popups
		self.popupsContainerVC = [[AGSPopupsContainerViewController alloc] initWithPopups:self.popups];
        //select the graphic for the first popup in the array
        self.currentPopup = [self.popups objectAtIndex:0];
        [self refreshSelection];
	}
	else {
        //The popupsContainerVC has already been initialized, Hence add additional popups from other layers.
		[self.popupsContainerVC showAdditionalPopups:self.popups];
	}
    
    if (self.popupView.frame.size.width < 100.0f) {
        [self.splitView setPosition:100.0f ofDividerAtIndex:0];
    }
    self.popupsContainerVC.view.frame = [self.popupView bounds];
    self.popupsContainerVC.delegate = self;
    
    //Add the PopupsContainer VC to the popup view
    [self.popupView addSubview:self.popupsContainerVC.view];
    self.popupsContainerVC.view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
}

-(void) resetPopupsAndSubviews {
    //Initialize Popups Array; If it already exists, remove all it's objects and reset it
	if (self.popups) {
		[self.popups removeAllObjects];
	}
	else {
		self.popups = [NSMutableArray array];
	}
    //Clear the popups from the popupsContainerVC so that the latter can be reused.
	if (self.popupsContainerVC) {
		[self.popupsContainerVC clearAllPopups];
	}
    //Reset the subview of the popupView
	if ([self.popupView.subviews count] > 0 ) {
        [self.popupView setSubviews:[NSArray array]];
	}
    
    self.currentPopup = nil;
    self.currentEditingPopup = nil;
    
    [self.splitView setPosition:0.0f ofDividerAtIndex:0];
}

- (void)respondToGeomChanged: (NSNotification*) notification {
    [self.popupsContainerVC geometryUpdated];
}

- (void) warnUserOfErrorWithMessage:(NSString*) message {
    //Display an alert to the user
    
    NSAlert *alert = [[NSAlert alloc] init];
    [alert addButtonWithTitle:@"Try Again"];
    [alert addButtonWithTitle:@"Cancel"];
    [alert setMessageText:@"Error"];
    [alert setInformativeText:message];
    [alert setAlertStyle:NSWarningAlertStyle];
   
    [alert beginSheetModalForWindow:self.view.window modalDelegate:self didEndSelector:@selector(featureEditingErrorAlertDidEnd:returnCode:contextInfo:) contextInfo:nil];

}

#pragma mark - NSSplitView delegate

// -------------------------------------------------------------------------------
//  splitView:constrainMaxCoordinate
//
//  this will restrict resize of splitView at specified max value
// -------------------------------------------------------------------------------
- (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)dividerIndex {
    return 260;
}

// -------------------------------------------------------------------------------
//  splitView:constrainMinCoordinate
//
//  this will restrict resize of splitView at specified min value
// -------------------------------------------------------------------------------
- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMin ofSubviewAt:(NSInteger)dividerIndex {
    return 200;
}

#pragma mark -

// -------------------------------------------------------------------------------
//  dealloc
// -------------------------------------------------------------------------------
- (void)dealloc {
    
    //remove observer
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

@end

//OBJECTIVE C SAMPLE CODE
/*
 Copyright 2013 Esri
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
 http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

#import <Cocoa/Cocoa.h>

@interface FeatureLayerEditingSample : NSViewController

<AGSMapViewLayerDelegate, AGSLayerDelegate, AGSMapViewTouchDelegate, AGSFeatureLayerEditingDelegate, AGSPopupsContainerDelegate, AGSAttachmentManagerDelegate>

@property (nonatomic, strong) IBOutlet NSButton *addFeatureButton;
@property (strong) IBOutlet AGSMapView *mapView;
@property (strong) IBOutlet NSView *popupView;
@property (strong) IBOutlet NSSplitView *splitView;

//IBActions - methods called by IBOutlets
- (IBAction)addFeature:(id)sender;

@end

Feedback on this topic?