Syncing Up

To apply local changes on the server, use one of the “sync up” methods. These methods update the server with data from the given SmartStore soup. They look for created, updated, or deleted records in the soup, and then replicate those changes on the server. The options argument specifies a list of fields to be updated. In Mobile SDK5.1 and later, you can override this field list by initializing the sync manager object with separate field lists for create and update operations. See Handling Field Lists in Create and Update Operations .

Locally created objects must include an “attributes” field that contains a “type” field that specifies the sObject type. For example, for an account named Acme, use: {Id:”local_x”, Name: Acme, attributes: {type:”Account”}}.

iOS: SFMobileSyncSyncManager Methods

  • You can create a named sync without running it.
    Swift
    1var syncState = syncManager.createSyncUp(target: target, options: options, 
    2    soupName: CONTACTS_SOUP, syncName: syncState.syncName)
    Objective-C
    1- (SFSyncState *)createSyncUp:(SFSyncUpTarget *)target 
    2                      options:(SFSyncOptions *)options 
    3                     soupName:(NSString *)soupName 
    4                     syncName:(NSString *)syncName;
  • You can create and run a sync with just options that uses the default target.
    Swift
    1var syncState = syncManager.syncUp(options: options, soupName: CONTACTS_SOUP, 
    2    onUpdate: updateFunc)
    Objective-C
    1- (SFSyncState*) syncUpWithOptions:(SFSyncOptions*)options 
    2                          soupName:(NSString*)soupName 
    3                       updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock;
  • You can create and run a sync based on a target that you configure in code.
    Swift
    1var syncState = syncManager.syncUp(options: options, soupName: CONTACTS_SOUP, 
    2    onUpdate: updateFunc)
    Objective-C
    1- (SFSyncState*) syncUpWithTarget:(SFSyncUpTarget*)target
    2                          options:(SFSyncOptions*)options
    3                         soupName:(NSString*)soupName
    4                      updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock;
  • Or, you can create and run a named sync. If you load a sync with the same name from a configuration file, this sync overrides it.
    Swift
    1var syncState = 
    2    try syncManager.syncUp(target: target, options: options, 
    3        soupName: CONTACTS_SOUP, syncName: syncState.syncName, 
    4        onUpdate: updateFunc)
    Objective-C
    1- (SFSyncState*) syncUpWithTarget:(SFSyncUpTarget*)target
    2                          options:(SFSyncOptions*)options
    3                         soupName:(NSString*)soupName
    4                         syncName:(NSString*)syncName
    5                      updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock
    6                            error:(NSError**)error;
  • To run or rerun an existing named sync configuration:
    Swift
    1open func reSync(named syncName: String, 
    2           onUpdate updateBlock: @escaping SyncUpdateBlock) throws -> SyncState
    Objective-C
    1- (nullable SFSyncState*) reSyncByName:(NSString*)syncName 
    2                           updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock
    3                                 error:(NSError**)error;
  • Or, to rerun a previous sync operation by sync ID:
    Swift
    1open func reSync(id syncId: NSNumber, 
    2      onUpdate updateBlock: @escaping SyncUpdateBlock) throws -> SyncState
    Objective-C
    1- (nullable SFSyncState*) reSync:(NSNumber*)syncId 
    2                     updateBlock:(SFSyncSyncManagerUpdateBlock)updateBlock
    3                           error:(NSError**)error;
  • To rerun a sync without getting progress updates, use this function from the Mobile Sync Swift extension:
    Swift
    1public func reSyncWithoutUpdates(named syncName: String, 
    2    _ completionBlock: @escaping (Result<SyncState, MobileSyncError>) -> Void)
    Objective-C
    (Not available.)
  • To sync up by external ID, see Syncing Up by External ID.

Android: SyncManager Methods

  • You can create a named sync up configuration without running it.
    1public SyncState createSyncUp(SyncUpTarget target, 
    2    SyncOptions options, 
    3    String soupName, 
    4    String syncName) 
    5    throws JSONException;
  • You can create and run an unnamed sync up configuration:

    1public SyncState syncUp(SyncUpTarget target, 
    2    SyncOptions options, 
    3    String soupName, 
    4    SyncUpdateCallback callback) 
    5    throws JSONException;
  • You can create and run a named sync up configuration:

    1public SyncState syncUp(SyncUpTarget target, 
    2    SyncOptions options, 
    3    String soupName, 
    4    String syncName, 
    5    SyncUpdateCallback callback) 
    6    throws JSONException;
  • To run or rerun an existing named sync configuration:
    1public SyncState reSync(String syncName, SyncUpdateCallback callback) 
    2    throws JSONException;
  • Or, to rerun a previous sync operation by sync ID:
    1public SyncState reSync(long syncId, SyncUpdateCallback callback) 
    2    throws JSONException;
  • To sync up by external ID, see Syncing Up by External ID.

Specifying Merge Modes

For sync up operations, you can specify a mergeMode option. You can choose one of the following behaviors:
  1. Overwrite server records even if they've changed since they were synced down to that client. When you call the syncUp method:
    • iOS: Set the options parameter to
      Swift
      1SyncOptions.newSyncOptions(forSyncUp: ["Name"], mergeMode: SyncMergeMode.overwrite)
      Objective-C
      [SFSyncOptions newSyncOptionsForSyncUp: ["Name"], mergeMode:SFSyncStateMergeModeOverwrite]
    • Android: Set the options parameter to SSyncOptions.optionsForSyncUp(fieldlist, SyncState.MergeMode.OVERWRITE)
    • Hybrid: Set the syncOptions parameter to {mergeMode:"OVERWRITE"}
  2. If any server record has changed since it was synced down to that client, leave it in its current state. The corresponding client record also remains in its current state. When you call the syncUp() method:
    • iOS: Set the options parameter to
      Swift
      1SyncOptions.newSyncOptions(forSyncUp: ["Name"], mergeMode: SyncMergeMode.leaveIfChanged)
      Objective-C
      [SFSyncOptions newSyncOptionsForSyncUp:fieldlist mergeMode:SFSyncStateMergeModeLeaveIfChanged]
    • Android: Set the options parameter to SyncOptions.optionsForSyncUp(fieldlist, SyncState.MergeMode.LEAVE_IF_CHANGED)
    • Hybrid: Set the syncOptions parameter to {mergeMode:"LEAVE_IF_CHANGED"}

If your local record includes the target’s modification date field, Mobile SDK detects changes by comparing it to the server record’s matching field. The default modification date field is lastModifiedDate. If your local records do not include the modification date field, the LEAVE_IF_CHANGED sync up operation reverts to an overwrite sync up.

The LEAVE_IF_CHANGED merge requires extra round trips to the server. More importantly, the status check and the record save operations happen in two successive calls. In rare cases, a record that is updated between these calls can be prematurely modified on the server.

Important

Example

iOS:

The MobileSyncExplorerSwift sample app demonstrates how to use named syncs and sync configuration files with the Salesforce Contact object. In iOS, this sample defines a ContactSObjectData class that represents a contact record as a Swift object. The sample also defines several support classes:
  • ContactSObjectDataSpec
  • SObjectData
  • SObjectDataSpec
  • SObjectDataFieldSpec
  • SObjectDataManager
To sync Contact data with the SmartStore soup, this app defines the following named sync operations in the Resources/usersyncs.json file:
1{
2  "syncs": [
3    {
4      "syncName": "syncDownContacts",
5      "syncType": "syncDown",
6      "soupName": "contacts",
7      "target": {"type":"soql", "query":"SELECT FirstName, LastName, Title, 
8          MobilePhone, Email, Department, HomePhone FROM Contact LIMIT 10000"},
9      "options": {"mergeMode":"OVERWRITE"}
10    },
11    {
12      "syncName": "syncUpContacts",
13      "syncType": "syncUp",
14      "soupName": "contacts",
15      "target": {"createFieldlist":["FirstName", "LastName", "Title", 
16          "MobilePhone", "Email", "Department", "HomePhone"]},
17      "options": {"fieldlist":["Id", "FirstName", "LastName", "Title", 
18          "MobilePhone", "Email", "Department", "HomePhone"], 
19          "mergeMode":"LEAVE_IF_CHANGED"}
20    }
21  ]
22}
In the RootViewController class, the syncUpDown() method starts the flow by calling the updateRemoteData(_:onFailure:) method of SObjectDataManager.
1func syncUpDown(){
2    let alert = self.showAlert("Syncing", message: "Syncing with Salesforce")
3    sObjectsDataManager.updateRemoteData({ [weak self] (sObjectsData) in
4        DispatchQueue.main.async {
5            alert.message = "Sync Complete!"
6            alert.dismiss(animated: true, completion: nil)
7            self?.refreshList()
8        }
9    }, onFailure: { [weak self] (error, syncState) in
10        alert.message = "Sync Failed!"
11        alert.dismiss(animated: true, completion: nil)
12        self?.refreshList()
13    })
14}
For the first argument of updateRemoteData, which represents success, syncUpDown passes a block that calls the refreshList() method of RootViewController. This method filters the local contacts according to customer input and refreshes the view.
updateRemoteData calls reSync using the syncUpContacts model—aliased here as kSyncUpName—. Syncing up ensures that allowed soup changes are merged into the Salesforce org.
1func updateRemoteData(_ onSuccess: @escaping ([SObjectData]) -> Void, 
2                        onFailure:@escaping (NSError?, SyncState) -> Void) -> Void {
3    do {
4        try self.syncMgr.reSync(named: kSyncUpName) { [weak self] (syncState) in
5            guard let strongSelf = self else {
6                return
7            }
8            switch (syncState.status) {
9            case .done:
10                do {
11                    let objects = try strongSelf.queryLocalData()
12                    strongSelf.populateDataRows(objects)
13                    try strongSelf.refreshRemoteData({ (sobjs) in
14                        onSuccess(sobjs)
15                    }, onFailure:  { (error,syncState) in
16                        onFailure(error,syncState)
17                    })
18                } catch let error as NSError {
19                    MobileSyncLogger.e(SObjectDataManager.self, 
20                        message: "Error with Resync \(error)" )
21                    onFailure(error,syncState)
22                }
23                break
24            case .failed:
25                MobileSyncLogger.e(SObjectDataManager.self, 
26                    message: "Resync \(syncState.syncName) failed" )
27                onFailure(nil,syncState)
28                break
29            default:
30                break
31            }
32        }
33    } catch {
34        onFailure(error as NSError, nil)
35    }
36}
If sync up succeeds—that is, if the SyncState status indicates “done”—several things happen:
  1. queryLocalData retrieves all raw data from the freshly updated soup.
    1let objects = try strongSelf.queryLocalData()
  2. populateDataRows transforms the soup’s data to ContactSObjectData objects and stores these objects in an internal array.
    1strongSelf.populateDataRows(objects)
  3. Control passes to refreshRemoteData(_:onFailure:). The refreshRemoteData method looks similar to updateRemoteData with two exceptions:
    • It performs a sync down instead of sync up.
    • If sync down succeeds, it “closes the circle” by executing the block that’s been passed to it from syncUpDown via updateRemoteData.
To summarize everything that happens in the syncUpDown call stack:
  1. Sync up: It syncs soup changes up to the server by calling updateRemoteData on SObjectsDataManager. This step ensures that all allowable local and offline changes are merged into Salesforce.
  2. Sync down: After the soup records are merged with server data, it syncs server data down to the soup through a call to refreshRemoteData. This step ensures that the soup reflects changes originating on the server and also changes merged from sync up. Remember: The sync up merge mode determines which soup edits are allowed on the server.
  3. Finally, it updates its UI with the updated contact records from the soup.

When you’re syncing records, always apply a sync up-sync down pair in the sequence demonstrated by the MobileSyncExplorerSwift sample app.

Important

Example

Android:

To sync up to the server, you call syncUp() with the same arguments as syncDown(): list of fields, name of source SmartStore soup, and an update callback. The only coding difference is that you can format the list of affected fields as an instance of SyncOptions instead of SyncTarget. Here’s the way it’s handled in the MobileSyncExplorer sample:

1public synchronized void syncUp() {
       
2    final SyncUpTarget target = new SyncUpTarget();
       
3    final SyncOptions options = 
4        SyncOptions.optionsForSyncUp(Arrays.asList(ContactObject.CONTACT_FIELDS_SYNC_UP),
               
5            MergeMode.LEAVE_IF_CHANGED);

	
6    try {
		
7        syncMgr.syncUp(target, options, ContactListLoader.CONTACT_SOUP, 
8            new SyncUpdateCallback() {

			
9                @Override
			
10                public void onUpdate(SyncState sync) {
		        
11                    if (Status.DONE.equals(sync.getStatus())) {
					
12                        syncDown();
				
13                    }
			
14                }
		
15            });
	
16    } catch (JSONException e) {
           
17        Log.e(TAG, "JSONException occurred while parsing", e);
	
18    } catch (MobileSyncException e) {
           
19        Log.e(TAG, "MobileSyncException occurred while attempting to sync up", e);
	
20    }

21}

In the internal SyncUpdateCallback implementation, this example takes the extra step of calling syncDown() when sync up is done. This step guarantees that the SmartStore soup remains up-to-date with any recent changes made to Contacts on the server.