Edit Offline

Download Sample Application

Take features offline, edit them and synchronize them back to an online service. This sample creates a local geodabase (.geodatabase) of the features extracted from an online feature service using methods of the AGSGDBSyncTask. The downloaded features are stored in a single geodatabase file and ,using this, your application can go offline allowing you to edit the features. Add new features and edit existing features using the supplied popup. Synchronize your edits back to the online service. This sample downloads all the features from layer(0) that fall within an area around San Francisco, California.

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 kEOOnlineFeatureServiceURL = "http://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/Wildfire/FeatureServer"
let kEOOnlineBaseMapURL = "http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"

class EditOfflineFeaturesSwiftSample: NSViewController, AGSMapViewTouchDelegate, AGSMapViewLayerDelegate, AGSLayerDelegate, AGSPopupsContainerDelegate, NSSplitViewDelegate {
    
    @IBOutlet weak var createGeodatabaseButton:NSButton!
    @IBOutlet weak var addFeatureButton:NSButton!
    @IBOutlet weak var editTextField:NSTextField!
    @IBOutlet weak var messageLabel:NSTextField!
    @IBOutlet weak var syncFeaturesButton:NSButton!
    
    @IBOutlet weak var mapView:AGSMapView!
    @IBOutlet weak var popupView:NSView!
    @IBOutlet weak var splitView:NSSplitView!
    
    var geodatabasePath:String!
    var geodatabaseTask:AGSGDBSyncTask!
    var areaOfInterest:AGSEnvelope!
    var geodatabaseJob:AGSCancellable!
    var geodatabase:AGSGDBGeodatabase!
    var localFeatureTable:AGSGDBFeatureTable!
    var localFeatureTableLayer:AGSFeatureTableLayer!
    var syncParameters:AGSGDBSyncParameters!
    var generateParameters:AGSGDBGenerateParameters!
    
    var featureLayer:AGSFeatureLayer!
    var sketchLayer:AGSSketchGraphicsLayer!
    var popupsContainerVC:AGSPopupsContainerViewController!
    var popups = Array<AGSPopup>()
    var mapPoint:AGSPoint!
    var currentPopup:AGSPopup!
    var currentEditingPopup:AGSPopup!
    var graphicsLayer:AGSGraphicsLayer!
    var originalFeatureAttributes:Dictionary<NSObject, AnyObject>!
    var originalGeometry:AGSGeometry!
    var inEditingMode = false
    var offline = 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: kEOOnlineBaseMapURL))
        baseMapLayer.delegate = self
        self.mapView.addMapLayer(baseMapLayer, withName:"Base Layer")
        
        //add feature layer to map
        let string = "\(kEOOnlineFeatureServiceURL)/0"
        self.featureLayer = AGSFeatureLayer(URL: NSURL(string: string), mode: .OnDemand)
        self.featureLayer.delegate = self
        self.featureLayer.outFields = ["*"]
        self.mapView.addMapLayer(self.featureLayer, withName:"Online Feature Layer")
        
        //add sketch graphics layer to map
        self.sketchLayer = AGSSketchGraphicsLayer()
        self.mapView.addMapLayer(self.sketchLayer, withName:"Sketch Graphics Layer")
        
        //Create a graphic showing the area of interest
        self.graphicsLayer = AGSGraphicsLayer()
        self.mapView.addMapLayer(self.graphicsLayer, withName:"Graphics Layer")
        let simpleFillSymbol = AGSSimpleFillSymbol(color: nil, outlineColor: NSColor.redColor())
        simpleFillSymbol.outline.width = 3.0
        self.areaOfInterest = AGSEnvelope(xmin: -13637696.600912, ymin:4539924.661945, xmax:-13624293.155808, ymax:4553328.107049, spatialReference:AGSSpatialReference.webMercatorSpatialReference())
        let graphic = AGSGraphic(geometry: self.areaOfInterest, symbol:simpleFillSymbol, attributes:nil)
        self.graphicsLayer.addGraphic(graphic)
        
        self.controlUI("createGDB")
        
        // zoom to area of interest
        self.mapView.zoomToGeometry(self.areaOfInterest, withPadding:400, animated:true)
    }
    
    //MARK: - Actions
    
    @IBAction func addFeature(sender:AnyObject) {
    
        if self.localFeatureTable != nil {
        
            let feature = self.localFeatureTable.featureWithType(self.localFeatureTable.types[0] as AGSFeatureType)
            
            self.resetPopupsAndSubviews()
            self.addPopupToPopupsForFeature(feature)
            self.populatePopupsContainer()
            self.popupsContainerVC.startEditingCurrentPopup()
            self.mapView.callout.hidden = true
            self.addFeatureButton.enabled = false
            self.syncFeaturesButton.enabled = false
        }
    }
    
    func generateGDB() {
        
        //Get path to store geodatabase in device/simulator
        let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
        self.geodatabasePath = paths[0] as String
        
        self.generateParameters = AGSGDBGenerateParameters(extent: self.areaOfInterest, layerIDs:[0])
        self.generateParameters.syncModel = .PerLayer
        self.generateParameters.outSpatialReference = self.mapView.spatialReference
        
        //disable the UI whilst the generate and download is happening
        self.controlUI("disableUI")
        
        self.geodatabaseJob = self.geodatabaseTask.generateGeodatabaseWithParameters(self.generateParameters, downloadFolderPath: self.geodatabasePath, useExisting: true, status: { [weak self] (status, userInfo) -> Void in
            if let weakSelf = self {
                weakSelf.messageLabel.stringValue = AGSResumableTaskJobStatusAsString(status)
            }
        }, completion: { [weak self] (geodatabase, error) -> Void in
            if let weakSelf = self {
                if error == nil {
                    weakSelf.mapView.removeMapLayerWithName("Online Feature Layer")
                    weakSelf.geodatabase = geodatabase
                    weakSelf.localFeatureTable = geodatabase.featureTables()[0] as AGSGDBFeatureTable
                    weakSelf.localFeatureTableLayer = AGSFeatureTableLayer(featureTable: weakSelf.localFeatureTable)
                    weakSelf.localFeatureTableLayer.allowHitTest =  true
                    weakSelf.localFeatureTableLayer.delegate = weakSelf
                    weakSelf.mapView.addMapLayer(weakSelf.localFeatureTableLayer, withName:"Offline Feature Layer")
                    weakSelf.controlUI("edit")
                    
                    let string = "The Job has completed and the geodatabase is located in this file: \(weakSelf.geodatabase.path)"
                    
                    weakSelf.messageLabel.stringValue = string
                } else {
                    weakSelf.showErrorWithTitle("Error fetching geodatabase!:", message:error.localizedDescription)
                    weakSelf.messageLabel.stringValue = "Error fetching geodatabase!"
                    weakSelf.createGeodatabaseButton.enabled = true
                }
            }
        })
    }
    
    @IBAction func createGeodatabase(sender:AnyObject) {
        
        self.controlUI("disableUI")
        
        //Create the task
        if self.geodatabaseTask != nil {
            self.generateGDB()
        }
        else {
            self.geodatabaseTask = AGSGDBSyncTask(URL: NSURL(string: kEOOnlineFeatureServiceURL))
        }
        self.geodatabaseTask.loadCompletion = { [weak self] (error) in
            if let weakSelf = self {
                if error != nil {
                    weakSelf.showErrorWithTitle("AGSGDBSyncTask failed to load!", message:error.localizedDescription)
                }
                else {
                    weakSelf.generateGDB()
                }
            }
        }
    }
    
    @IBAction func syncFeatures(sender:AnyObject) {
        if let viewWindow = self.view.window {
            let alert = NSAlert()
            alert.addButtonWithTitle("Upload")
            alert.addButtonWithTitle("Cancel")
            alert.informativeText = "This operation cannot be undone."
            alert.messageText = "Are you sure you want to upload these feature edits?"
            alert.alertStyle = .WarningAlertStyle
            alert.beginSheetModalForWindow(viewWindow, modalDelegate:self, didEndSelector:"syncAlertDidEnd:returnCode:", contextInfo:nil)
        }
    }
    
    
    //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 editing continue sketching
        if self.inEditingMode {
            self.sketchLayer.mapView(mapView, didClickAtPoint:screen, mapPoint:mappoint, features:features)
        }
        
        //If not editing then show the popup for the Offline Feature Layer
        else {
            self.resetPopupsAndSubviews()
    
            if self.offline && features.count > 0 {
                if let layerFeatures = features["Offline Feature Layer"] as? Array<AGSGDBFeature> {
                    for feature in layerFeatures {
                        self.addPopupToPopupsForFeature(feature)
                        self.localFeatureTableLayer.setSelected(true, forFeature:feature)
                    }
                    if self.popups.count > 0 {
                        self.populatePopupsContainer()
                    }
                }
            }
        }
    }
    
    //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 buttons
        self.addFeatureButton.enabled = false
        self.syncFeaturesButton.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
        self.syncFeaturesButton.enabled = true
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, didChangeToCurrentPopup popup: AGSPopup!) {
        // change current popup
        // set selection
        self.currentPopup = popup
        self.refreshSelection()
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, didCancelEditingForPopup popup: AGSPopup!) {
        
        self.currentEditingPopup = popup
        self.cancelEditing()
        
        // set in editing
        self.inEditingMode = false
        
        // enable buttons
        self.addFeatureButton.enabled = true
        self.syncFeaturesButton.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.gdbFeatureTable.geometryType, self.mapView.spatialReference)
    }
    
    func popupsContainer(popupsContainer: AGSPopupsContainer!, didDeleteForPopup popup: AGSPopup!) {
        self.resetPopupsAndSubviews()
    }
    
    //MARK: - Popup Helper Methods
    
    func addPopupToPopupsForFeature(feature:AGSGDBFeature) {
        // add popup to popups array
        self.popupView.subviews = Array()
        let popupInfo = AGSPopupInfo(forGDBFeatureTable: feature.table)
        popupInfo.title = feature.table.tableName()
        let popup = AGSPopup(GDBFeature: feature, popupInfo: popupInfo)
        self.popups.append(popup)
    }
    
    func cancelEditing() {
        
        if self.currentEditingPopup.feature.geometry == nil {
            self.resetPopupsAndSubviews()
        }
        
        if self.currentEditingPopup != nil {
            self.currentEditingPopup.feature.setAttributes(self.originalFeatureAttributes)
            self.currentEditingPopup.feature.geometry = self.originalGeometry
            self.currentEditingPopup = nil
        }
        self.sketchLayer.clear()
        self.mapView.touchDelegate = self
    }
    
    func syncAlertDidEnd(alert:NSAlert, returnCode:(Int)) {
    
        alert.window.orderOut(nil)
        
        if returnCode == NSAlertFirstButtonReturn {
        
            self.resetPopupsAndSubviews()
            self.sketchLayer.clear()
            
            if self.geodatabaseTask != nil && self.geodatabase != nil {
            
                let syncParams = AGSGDBSyncParameters(geodatabase: self.geodatabase)
                
                self.controlUI("disableUI")
                
                self.geodatabaseTask.syncGeodatabase(self.geodatabase, params: syncParams, status: { [weak self] (status, userInfo) -> Void in
                    if let weakSelf = self {
                        weakSelf.messageLabel.stringValue = AGSResumableTaskJobStatusAsString(status)
                    }
                }, completion: { [weak self] (featureEditErrors, syncError) -> Void in
                    if let weakSelf = self {
                        if syncError != nil {
                            weakSelf.showErrorWithTitle("GDB Sync Failed!", message:syncError.localizedDescription)
                            weakSelf.messageLabel.stringValue = "GDB Sync Failed! \(syncError.localizedDescription)"
                        }
                        else{
                            weakSelf.messageLabel.stringValue = "Features uploaded successfully"
                            weakSelf.mapView.removeMapLayerWithName("Offline Feature Layer")
                            weakSelf.mapView.addMapLayer(weakSelf.featureLayer, withName:"Online Feature Layer")
                            weakSelf.featureLayer.refresh()
                            weakSelf.controlUI("createGDB")
                        }
                    }
                })
            }
        }
    }
    
    func finishEditingPopup(popup: AGSPopup) {
        
        self.localFeatureTable = popup.gdbFeatureTable
        self.currentEditingPopup = popup
        self.currentPopup = popup
        self.mapView.touchDelegate = self
        self.sketchLayer.geometry = nil
        self.sketchLayer.clear()
        
        // reselect the graphic associated with the Popup
        self.refreshSelection()
        
        // set in editing
        self.inEditingMode = false
        
        // enable buttons
        self.addFeatureButton.enabled = true
        self.syncFeaturesButton.enabled = true
    }
    
    func refreshSelection() {
        self.localFeatureTableLayer.clearSelection()
        self.localFeatureTableLayer.setSelected(true, forFeature:self.currentPopup.feature)
    }
    
    func populatePopupsContainer() {
        
        if self.popupsContainerVC == nil || self.popupsContainerVC.popups.count == 0 {
            // initialize the popups container VC with 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 {
            // show additional popups.
            self.popupsContainerVC.showAdditionalPopups(self.popups)
        }
        
        self.popupsContainerVC.view.frame = self.popupView.bounds
        self.popupsContainerVC.delegate = self
        self.popupsContainerVC.view.autoresizingMask = .ViewWidthSizable | .ViewHeightSizable;
        self.popupView.addSubview(self.popupsContainerVC.view)
    }
    
    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<AGSPopup>()
        }
        // remove all popups from vc
        if self.popupsContainerVC != nil {
            self.popupsContainerVC.clearAllPopups()
        }
        // reset subviews of popupView
        if self.popupView.subviews.count > 0 {
            self.popupView.subviews = Array()
        }
        
        // clear selected features
        self.localFeatureTableLayer.clearSelection()
        
        // rest current popup and editing popup
        self.currentPopup = nil
        self.currentEditingPopup = nil
    }
    
    func respondToGeomChanged(notification: NSNotification) {
        self.popupsContainerVC.geometryUpdated()
    }
        
    //MARK: - Extra Methods
    
    func controlUI(uiString: String) {
        
        self.createGeodatabaseButton.enabled = false
        self.addFeatureButton.hidden = true
        self.syncFeaturesButton.hidden = true
        self.editTextField.hidden = true
        
        if uiString == "createGDB" {
            self.createGeodatabaseButton.enabled = true
            self.offline = false
        }
        else if uiString == "edit" {
            self.offline = true
            self.addFeatureButton.hidden = false
            self.syncFeaturesButton.hidden = false
            self.editTextField.hidden = false
        }
    }
    
    func showErrorWithTitle(title:String, message:String) {
        if let viewWindow = self.view.window {
            let alert = NSAlert()
            alert.messageText = title
            alert.informativeText = message
            alert.beginSheetModalForWindow(viewWindow, modalDelegate:self, didEndSelector:nil, 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 "EditOfflineFeaturesSample.h"

#define kOnlineFeatureServiceURL @"http://services.arcgis.com/P3ePLMYs2RVChkJx/ArcGIS/rest/services/Wildfire/FeatureServer"
#define kOnlineBaseMapURL @"http://services.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer"

@interface EditOfflineFeaturesSample ()

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

@end

@implementation EditOfflineFeaturesSample


// -------------------------------------------------------------------------------
//  awakeFromNib
// -------------------------------------------------------------------------------
- (void)awakeFromNib {
    
    //enable wrap around
    [self.mapView enableWrapAround];
    
    //set mapView delegates
    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:kOnlineBaseMapURL]];
    baseMapLayer.delegate = self;
    [self.mapView addMapLayer:baseMapLayer withName:@"Base Layer"];
    
    //add feature layer to map
    NSMutableString *string = [NSMutableString stringWithString:kOnlineFeatureServiceURL];
    [string appendString:@"/0"];
	self.featureLayer = [[AGSFeatureLayer alloc] initWithURL:[NSURL URLWithString:string] mode:AGSFeatureLayerModeOnDemand];
    self.featureLayer.delegate = self;
    self.featureLayer.outFields = @[@"*"];
	[self.mapView addMapLayer:self.featureLayer withName:@"Online Feature Layer"];

    //add sketch graphics layer to map
    self.sketchLayer = [[AGSSketchGraphicsLayer alloc]init];
    [self.mapView addMapLayer:self.sketchLayer withName:@"Sketch Graphics Layer"];

    //Create a graphic showing the area of interest
    self.graphicsLayer = [AGSGraphicsLayer graphicsLayer];
    [self.mapView addMapLayer:self.graphicsLayer withName:@"Graphics Layer"];
    AGSSimpleFillSymbol *simpleFillSymbol = [AGSSimpleFillSymbol simpleFillSymbolWithColor:nil outlineColor:[NSColor redColor]];
    simpleFillSymbol.outline.width = 3.0;
    self.areaOfInterest = [AGSEnvelope envelopeWithXmin: -13637696.600912 ymin:4539924.661945 xmax:-13624293.155808 ymax:4553328.107049 spatialReference:[AGSSpatialReference webMercatorSpatialReference]];
    AGSGraphic *graphic = [AGSGraphic graphicWithGeometry:self.areaOfInterest symbol:simpleFillSymbol attributes:nil];
    [self.graphicsLayer addGraphic:graphic];

    self.offline = NO;
    self.inEditingMode = NO;
    [self controlUI:@"createGDB"];

    // zoom to area of interest
    [self.mapView zoomToGeometry:self.areaOfInterest withPadding:400 animated:YES];
}

#pragma mark - Actions


- (void)addFeature:(id)sender {
	
    if (self.localFeatureTable) {
        
        AGSGDBFeature *feature = [self.localFeatureTable featureWithType:self.localFeatureTable.types[0]];
        
        [self resetPopupsAndSubviews];
		[self addPopupToPopupsForFeature:feature];
		[self populatePopupsContainer];
        [self.popupsContainerVC startEditingCurrentPopup];
        self.mapView.callout.hidden = YES;
        [self.addFeatureButton setEnabled:NO];
        [self.syncFeaturesButton setEnabled:NO];
	}
}

- (void)generateGDB {
    
    __weak EditOfflineFeaturesSample *weakSelf = self;
    
    //Get path to store geodatabase in device/simulator
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    self.geodatabasePath = [paths objectAtIndex:0];
    
    self.generateParameters = [[AGSGDBGenerateParameters alloc]initWithExtent:self.areaOfInterest layerIDs:@[@0]];
    self.generateParameters.syncModel = AGSGDBSyncModelPerLayer;
    self.generateParameters.outSpatialReference = self.mapView.spatialReference;
    
    //disable the UI whilst the generate and download is happening
    [self controlUI:@"disableUI"];
    
    self.geodatabaseJob = [self.geodatabaseTask generateGeodatabaseWithParameters:self.generateParameters downloadFolderPath:self.geodatabasePath
                                                                      useExisting:YES
                                                                       status:^(AGSResumableTaskJobStatus status, NSDictionary *userInfo)
                                                                            {weakSelf.messageLabel.stringValue = AGSResumableTaskJobStatusAsString(status);}
                                                                       completion:^(AGSGDBGeodatabase *geodatabase, NSError *error){
                                                                           if (!error) {
                                                                               [weakSelf.mapView removeMapLayerWithName:@"Online Feature Layer"];
                                                                               weakSelf.geodatabase = geodatabase;
                                                                               weakSelf.localFeatureTable = [[geodatabase featureTables] objectAtIndex:0];
                                                                               weakSelf.localFeatureTableLayer = [[AGSFeatureTableLayer alloc] initWithFeatureTable: weakSelf.localFeatureTable];
                                                                               weakSelf.localFeatureTableLayer.allowHitTest =  YES;
                                                                               weakSelf.localFeatureTableLayer.delegate = self;
                                                                               [weakSelf.mapView addMapLayer:weakSelf.localFeatureTableLayer withName:@"Offline Feature Layer"];
                                                                               [weakSelf controlUI:@"edit"];
                                                                               
                                                                               NSMutableString *string = [NSMutableString string];
                                                                               [string appendString:@"The Job has completed and the geodatabase is located in this file: "];
                                                                               [string appendString:self.geodatabase.path];
                                                                               
                                                                               weakSelf.messageLabel.stringValue = string;
                                                                           } else {
                                                                               [weakSelf showErrorWithTitle:@"Error fetching geodatabase!:" message:[NSString stringWithFormat:@"%@",error]];
                                                                               weakSelf.messageLabel.stringValue = @"Error fetching geodatabase!";
                                                                               [weakSelf.createGeodatabaseButton setEnabled:YES];
                                                                           }
                                                                       }];
}

-(IBAction)createGeodatabase:(id)sender {
    
    [self controlUI:@"disableUI"];
    __weak EditOfflineFeaturesSample *weakSelf = self;
    
    //Create the task
    if (self.geodatabaseTask) {
        [self generateGDB];
    }
	else {
		self.geodatabaseTask = [[AGSGDBSyncTask alloc] initWithURL:[NSURL URLWithString:kOnlineFeatureServiceURL] credential:nil];
    }
    self.geodatabaseTask.loadCompletion = ^(NSError *error){
        if (error) {
            [weakSelf showErrorWithTitle:@"AGSGDBSyncTask failed to load!" message:[NSString stringWithFormat:@"%@",error]];
        } else {
            [weakSelf generateGDB];
        }
    };
}

-(IBAction)syncFeatures:(id)sender {
    NSAlert *alert = [[NSAlert alloc] init];
    [alert addButtonWithTitle:@"Upload"];
    [alert addButtonWithTitle:@"Cancel"];
    [alert setInformativeText:@"This operation cannot be undone."];
    [alert setMessageText:@"Are you sure you want to upload these feature edits?"];
    [alert setAlertStyle:NSWarningAlertStyle];
    [alert beginSheetModalForWindow:self.view.window modalDelegate:self didEndSelector:@selector(syncAlertDidEnd:returnCode:contextInfo:) contextInfo:nil];
}


#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 editing continue sketching
    if (self.inEditingMode) {
        [self.sketchLayer mapView:mapView didClickAtPoint:screen mapPoint:mappoint features:features];
    }
    
    //If not editing then show the popup for the Offline Feature Layer
    else {
        
        [self resetPopupsAndSubviews];

        if (self.offline && features.count > 0) {
            for (AGSGDBFeature *feature in [features valueForKey:@"Offline Feature Layer"]) {
                [self addPopupToPopupsForFeature:feature];
                [self.localFeatureTableLayer setSelected:YES forFeature:feature];
            }
            if ([self.popups count] > 0) {
                [self populatePopupsContainer];
            }
        }
    }
}

#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];
    
    // set toch delegate to sketch layer
    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 {
    
    // store original attribute and geometry
    // so can revert back if editing is canceled
    self.originalFeatureAttributes = [popup.feature allAttributes];
    self.originalGeometry = [popup.feature.geometry mutableCopy];
    
    // set in editing
    self.inEditingMode = YES;
    
    // disable buttons
    self.addFeatureButton.enabled = NO;
    self.syncFeaturesButton.enabled = NO;
}

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer didFinishEditingForPopup:(AGSPopup*)popup {

    // finish editing
    [self finishEditingPopup:popup];
    
    // set in editing
    self.inEditingMode = NO;
    
    // enable buttons
    self.addFeatureButton.enabled = YES;
    self.syncFeaturesButton.enabled = YES;
}

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

-(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;
    self.syncFeaturesButton.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(popup.gdbFeatureTable.geometryType, self.mapView.spatialReference);
}

-(void)popupsContainer:(id<AGSPopupsContainer>)popupsContainer didDeleteForPopup:(AGSPopup *)popup {
    [self resetPopupsAndSubviews];
}

#pragma mark - Popup Helper Methods

- (void)addPopupToPopupsForFeature:(AGSGDBFeature *)feature {
    // add popup to popups array
	[self.popupView setSubviews:[NSArray array]];
	AGSPopupInfo *popupInfo = [AGSPopupInfo popupInfoForGDBFeatureTable:feature.table];
	popupInfo.title = feature.table.tableName;
	AGSPopup *popup = [AGSPopup popupWithGDBFeature:feature popupInfo:popupInfo];
	[self.popups addObject:popup];
}

-(void)cancelEditing {
    
    if (self.currentEditingPopup.feature.geometry == nil) {
        [self resetPopupsAndSubviews];
    }
    
    [self.currentEditingPopup.feature setAttributes:self.originalFeatureAttributes];
    self.currentEditingPopup.feature.geometry = self.originalGeometry;
    self.currentEditingPopup = nil;
    [self.sketchLayer clear];
    self.mapView.touchDelegate = self;
}

-(void)syncAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo{
    
    [[alert window]orderOut:nil];
    
    if (returnCode == NSAlertFirstButtonReturn){
        
        [self resetPopupsAndSubviews];
        [self.sketchLayer clear];
        
        __weak EditOfflineFeaturesSample *weakSelf = self;
        
        if (self.geodatabaseTask && self.geodatabase) {
            
            AGSGDBSyncParameters *syncParams = [[AGSGDBSyncParameters alloc] initWithGeodatabase:self.geodatabase];
            
            [self controlUI:@"disableUI"];
            
            [self.geodatabaseTask syncGeodatabase:self.geodatabase params:syncParams
                                           status:^(AGSResumableTaskJobStatus status, NSDictionary *userInfo){
                                               weakSelf.messageLabel.stringValue = AGSResumableTaskJobStatusAsString(status);
                                           }
                                           completion:^(AGSGDBEditErrors *featureEditErrors, NSError* syncError) {
                                           if (syncError){
                                               [weakSelf showErrorWithTitle:@"GDB Sync Failed!" message:[NSString stringWithFormat:@"%@",syncError]];
                                               weakSelf.messageLabel.stringValue = @"GDB Sync Failed! %@", [NSString stringWithFormat:@"%@",syncError];
                                           }
                                           else{
                                               weakSelf.messageLabel.stringValue = @"Features uploaded successfully";
                                               [weakSelf.mapView removeMapLayerWithName:@"Offline Feature Layer"];
                                               [weakSelf.mapView addMapLayer:weakSelf.featureLayer withName:@"Online Feature Layer"];
                                               [weakSelf.featureLayer refresh];
                                               [weakSelf controlUI:@"createGDB"];
                                           }
                                       }
             
            ];
        }
        
    }
}

- (void)finishEditingPopup:(AGSPopup *)popup {
    
    self.localFeatureTable = popup.gdbFeatureTable;
    self.currentEditingPopup = popup;
    self.currentPopup = popup;
    self.mapView.touchDelegate = self;
    self.sketchLayer.geometry = nil;
    [self.sketchLayer clear];
    
    // reselect the graphic associated with the Popup
    [self refreshSelection];
    
    // set in editing
    self.inEditingMode = NO;
    
    // enable buttons
    self.addFeatureButton.enabled = YES;
    self.syncFeaturesButton.enabled = YES;
}

-(void)refreshSelection {
    [self.localFeatureTableLayer clearSelection];
    [self.localFeatureTableLayer setSelected:YES forFeature:self.currentPopup.feature];
}

-(void) populatePopupsContainer {
    
	if (!self.popupsContainerVC || ([self.popupsContainerVC.popups count] == 0)) {
        // initialize the popups container VC with 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 {
        // show additional popups.
		[self.popupsContainerVC showAdditionalPopups:self.popups];
	}
    
    self.popupsContainerVC.view.frame = [self.popupView bounds];
    self.popupsContainerVC.delegate = self;
    self.popupsContainerVC.view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
    [self.popupView addSubview:self.popupsContainerVC.view];
}

-(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];
	}
    // remove all popups from vc
	if (self.popupsContainerVC) {
		[self.popupsContainerVC clearAllPopups];
	}
    // reset subviews of popupView
	if ([self.popupView.subviews count] > 0 ) {
        [self.popupView setSubviews:[NSArray array]];
	}
    
    // clear selected features
    [self.localFeatureTableLayer clearSelection];
    
    // rest current popup and editing popup
    self.currentPopup = nil;
    self.currentEditingPopup = nil;
}

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

#pragma mark - Extra Methods

- (void)controlUI:(NSString*)uiString {
    
    self.createGeodatabaseButton.enabled = NO;
    self.addFeatureButton.hidden = YES;
    self.syncFeaturesButton.hidden = YES;
    self.editTextField.hidden = YES;
    
    if ([uiString  isEqual: @"createGDB"]) {
        self.createGeodatabaseButton.enabled = YES;
        self.offline = NO;
    }
    else if ([uiString  isEqual: @"edit"]) {
        self.offline = YES;
        self.addFeatureButton.hidden = NO;
        self.syncFeaturesButton.hidden = NO;
        self.editTextField.hidden = NO;
    }
}

- (void)showErrorWithTitle:(NSString*)title message:(NSString*)message {
    NSAlert *alert = [[NSAlert alloc] init];
    [alert setMessageText:title];
    [alert setInformativeText:message];
    [alert beginSheetModalForWindow:self.view.window modalDelegate:self didEndSelector:nil 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 350;
}

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

#pragma mark -

- (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 EditOfflineFeaturesSample : NSViewController

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

@property (strong) IBOutlet NSButton *createGeodatabaseButton;
@property (strong) IBOutlet NSButton *addFeatureButton;
@property (strong) IBOutlet NSTextField *editTextField;
@property (strong) IBOutlet NSTextField *messageLabel;
@property (strong) IBOutlet NSButton *syncFeaturesButton;

@property (strong) IBOutlet AGSMapView *mapView;
@property (strong) IBOutlet NSView *popupView;
@property (strong) IBOutlet NSSplitView *splitView;

@property (strong) NSString *geodatabasePath;
@property (strong) AGSGDBSyncTask *geodatabaseTask;
@property (strong) AGSEnvelope *areaOfInterest;
@property (strong) id<AGSCancellable> geodatabaseJob;
@property (strong) AGSGDBGeodatabase *geodatabase;
@property (strong) AGSGDBFeatureTable *localFeatureTable;
@property (strong) AGSFeatureTableLayer *localFeatureTableLayer;
@property (strong) AGSGDBSyncParameters *syncParameters;
@property (strong) AGSGDBGenerateParameters *generateParameters;


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

@end

Feedback on this topic?