SAPOfflineOData Reference


Use the SAPOfflineOData component to add offline capability to your application.

Dependencies

The SAPOfflineOData.framework depends on the following frameworks:

  • SAPCommon.framework
  • SAPFoundation.framework
  • SAPOData.framework

Installation

Add SAPFoundation.framework, SAPOData.framework, SAPCommon.framework and SAPOfflineOData.framework to the Xcode project for your application. Make sure Copy Items is selected for the non-system frameworks.

Your project is now linked to the frameworks and they should appear under Link Binary With Libraries in the Build Phases tab of the project settings.

Prerequisites

Perform the tasks described in Offline OData Task Flow.

Usage

The main entry point is the OfflineODataProvider class.

To create instance of this class, you must have:

See SAPURLSession API documentation for more information.

Initialization

The sample code below demonstrates the approach for initializing OfflineODataProvider and OfflineDataServiceAsync, which provides asynchronous (async) functions in Swift. Use OfflineDataService if you prefer using functions with completion handlers and Swift’s Result type.

/// Note: you need to replace variable values below starting with "YOUR" with your own values.
let storeName = "YOUR_STORE_NAME"
let host = "YOUR_SERVER_HOST"
let port = "YOUR_PORT"
let serviceName = "YOUR_SERVICE_NAME"
let myServiceRoot = URL( string: "https://\(host):\(port)/\(serviceName)/" )!

/// Setup an instance of OfflineODataParameters. See OfflineODataParameters class document for details.
/// You can also set storeParams.storePath if the default location is not desired.
var storeParams = OfflineODataParameters()
storeParams.enableRepeatableRequests = true
storeParams.storeName = storeName

/// CPmsURLSessionDelegate is your implemetation of SAPURLSessionDelegate for handling authentication or other tasks.
let cpmsURLSessionDelegate = CPmsURLSessionDelegate()

/// Setup an instance of SAPURLSession for authentication. See SAPURLSession class document for more details.
let mySAPURLSession = SAPURLSession( configuration: URLSessionConfiguration.default, delegate: cpmsURLSessionDelegate )
mySAPURLSession.register( SAPcpmsObserver( applicationID: "YOUR_APP_ID" ) )

/// Create the OfflineODataProvider and OfflineDataServiceAsync instances.
let provider = try OfflineODataProvider( serviceRoot: myServiceRoot, parameters: storeParams, sapURLSession: mySAPURLSession )
let service = OfflineDataServiceAsync( provider: provider.syncProvider )

/// A defining query should be a valid OData query optionally relative to the
/// service root, e.g., an entity set name "Customers".
/// See OfflineODataDefiningQuery for more details.
let customersDQ = OfflineODataDefiningQuery( name: "CustomersQuery", query: "Customers", automaticallyRetrievesStreams: false )
let suppliersDQ = OfflineODataDefiningQuery( name: "SuppliersQuery", query: "Suppliers", automaticallyRetrievesStreams: false )
try provider.add( definingQuery: customersDQ )
try provider.add( definingQuery: suppliersDQ )

/// Alternatively, create the defining queries using OfflineDataServiceAsync. See OfflineDataServiceAsync.createDownloadQuery(name:query:streams:) for more details.
// try await service.createDownloadQuery(name: "CustomersQuery", query: DataQuery().withURL("Customers"), streams: false)
// try await service.createDownloadQuery(name: "SuppliersQuery", query: DataQuery().withURL("Suppliers"), streams: false)

Optionally, you can provide a delegate when creating the OfflineODataProvider instance. For example,

/// Set up a delegate instance. See sample code below for definition of the OfflineODataDelegateSample class.
let myDelegate = OfflineODataDelegateSample()
let provider = try OfflineODataProvider( serviceRoot: myServiceRoot, parameters: storeParams, sapURLSession: mySAPURLSession, delegate: myDelegate )

The protocol OfflineODataDelegate is deprecated. We recommend to use the new protocol OfflineODataProviderDelegate when you build your application with the new library.

Here is a sample implementation of OfflineODataProviderDelegate:

class OfflineODataDelegateSample : OfflineODataProviderDelegate
{
    public func offlineODataProvider( _ provider: OfflineODataProvider, didUpdateOpenProgress progress: OfflineODataProviderOperationProgress ) -> Void
    {
        print("Open store step \(progress.currentStepNumber)/\(progress.totalNumberOfSteps): \(progress.step) with message: \(progress.defaultMessage)")
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, didUpdateDownloadProgress progress: OfflineODataProviderDownloadProgress ) -> Void
    {
        print("Download store step \(progress.currentStepNumber)/\(progress.totalNumberOfSteps): \(progress.step) with message: \(progress.defaultMessage)")
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, didUpdateUploadProgress progress: OfflineODataProviderOperationProgress ) -> Void
    {
        print("Upload store step \(progress.currentStepNumber)/\(progress.totalNumberOfSteps): \(progress.step) with message: \(progress.defaultMessage)")
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, requestDidFail request: OfflineODataFailedRequest ) -> Void
    {
        print("Request failed. HTTP status \(request.httpStatusCode), message: \(request.errorMessage)")
    }

    public func offlineODataProvider( _ provider: OfflineODataProvider, didUpdateSendStoreProgress progress: OfflineODataProviderOperationProgress ) -> Void
    {
        print("Send store step \(progress.currentStepNumber)/\(progress.totalNumberOfSteps): \(progress.step) with message: \(progress.defaultMessage)")
    }
}

Open and Close the OfflineDataServiceAsync

The OfflineDataServiceAsync needs to be opened before performing any operations. It should be closed when it is no longer needed.

/// Open the offline store. The open() function is asynchronous.
do {
    try await service.open()
    /// Do whatever is needed after the store is successfully opened. This can be as simple as printing a message.
    print( "Store is successfully opened" )
} catch {
   /// Do whatever is needed if opening store is failed. This can be as simple as printing a message.
   print( "Store open failed" )
}

Once the offline store is opened, various operations can be performed, e.g., executing queries. See more examples in sections below.

The offline store needs to be closed if it is no longer needed:

/// Close the offline store.
try await service.close()

Operations

For most operations, OfflineDataServiceAsync behaves similarly to OnlineDataServiceAsync.

For example, you can retrieve entities easily if you have generated proxy classes from metadata (e.g., you have Customer class):

/// In the following samples, the "service" variable is an instance of a generated proxy class extending OfflineDataServiceAsync that takes the syncProvider property of the OfflineODataProvider instance as its provider.
let customers = try await service.fetchCustomers( matching: DataQuery().orderBy( Customer.customerID ) )

for customer in customers {
    /// Handle the customer, e.g., print information of the customer.
    ...
}

Uploading and Downloading Offline Data

  • Upload: update the client’s back end through SAP Mobile Services.
  • Download: update the client’s offline store from the back end.

For additional information, see Synchronizing Data.

The sample code below shows how to create entities (using dynamic API) and perform an upload and download.

let customersSet: EntitySet = try service.entitySet( withName: "Customers" )
let customerType: EntityType = customersSet.entityType

/// Declare a new customer.
let customer = EntityValue.ofType( customerType ).inSet( customersSet )

/// Set property values.
customerType.property( withName: "CustomerId" ).setStringValue( in: customer, to: "..." )
customerType.property( withName: "FirstName" ).setStringValue( in: customer, to: "..." )
...

/// Create the new customer.
try service.createEntity( customer )

/// Now we have a local customer.
/// We can use Offline specific isLocal function to find local entities.
/// See OfflineODataQueryFunction for more available functions.
let localCustomersQuery = DataQuery().from( customersSet ).filter( OfflineODataQueryFunction.isLocal() )
let localCustomers = try await service.fetchCustomers( matching: localCustomersQuery )

/// Process local customers
...

/// Upload local changes. After uploading we still have the local customer.
do {
    try await service.upload()
    print( "Upload succeeded." )
} catch { 
    print( "Upload failed." )
}

Perform partial download after uploading local changes:

/// Download the Customers defining query only. After this there would be no local customers
/// (but there may be local entities of other types if you have modified any).
do {
  try await service.download( groups: StringList.of("CustomersQuery") )
  print("Download succeeded.")
} catch {
  print("Download failed.")
}

Perform full download:

/// Download all. After this no entities would be local.
do {
  try await service.download()
  print("Download succeeded.")
} catch {
  print("Download failed.")
}

ErrorArchive

Unlike Online OData where errors are discovered immediately, Offline OData errors are not discovered until the upload operation is performed. When a request fails against the OData back end during an upload operation, the request and any relevant details are stored in the ErrorArchive, a special entity set that can be queried using the OfflineODataProvider. App developers must determine what to do for these errors.

For additional information, see Error Handling.

The following sample code illustrates how to retrieve and delete errors from the ErrorArchive.

var errorArray = try provider.fetchErrorArchive()
for error in errorArray {
    try service.loadProperty( OfflineODataErrorArchiveEntity.affectedEntity, into: error )
    let errorEntity = error.affectedEntity
    let errorType = error.dataType as! StructureType
    let customerId = errorType.property( withName: "CustomerId" ).stringValue( from: errorEntity! )

    /// Your code for handling error entity goes here
    ...
}

/// Remove errors (assuming that there is at least one error).
let error = errorArray[ 0 ]
try service.deleteEntity( error )
errorArray = try service.fetchErrorArchive()
if( errorArray.count == 0 ) {
    /// Completing error handling
    ...
}

Alternatively, use functions in OfflineDataServiceAsync to achieve the same. See PendingRequest.

/// Check if there are failed requests.
try service.hasFailedRequests()

/// Get the list of failed requests.
var failedRequests = try await service.failedRequests()

/// Handling of the failed requests
for request in failedRequests {
    /// Check the error message
    let errorResponse = try failedRequest.errorResponse()

    /// Fix the error entity (assuming there is an error in errorProp)
    let errorEntity = request.affectedEntity(fixRequest: true)!
    errorEntity?.entityType.property(withName: "errorProp").setStringValue( in: errorEntity!, to: "corrected" )
    try service.updateEntity( errorEntity )

    /// Or simply delete the failed request
    // let errorEntity = request.affectedEntity()!
    // errorEntity.cancel()
    ...
}

EventLog

Offline OData provides a query-able event log which contains information about Offline OData events (such as downloads and uploads). The event log is represented as a read-only entity set, EventLog, which can be read and queried using an OfflineODataProvider.

Below is sample code for reading the event log:

let eventLogArray = try provider.fetchEventLog()
for eventLog in eventLogArray {
    let eventID = eventLog.id
    let eventDetails = eventLog.details
    let eventType = eventLog.eventType
    let eventTime = eventLog.time

    /// Your code for handling event log goes here
    ...
}

Alternatively, use functions in OfflineDataServiceAsync to achieve the same. See SyncEvent.

let syncEventList = try await service.eventHistory()
for syncEvent in syncEventList {
    let eventID = syncEvent.eventID
    let eventDetails = syncEvent.details
    let eventType = syncEvent.type
    let eventTime = syncEvent.time

    /// Your code for handling event log goes here
    ...
}