📢 Attention Salesforce Certified Trailblazers! Maintain your credentials and link your Trailhead and Webassessor accounts by April 19th. Learn more.
close

Modify the Forceios Native App

Learning Objectives

After completing this unit, you'll be able to:

  • Customize the REST request of the native iOS template app.
  • Add a swipe gesture and a button so users can use the list view to delete Salesforce records.
  • Handle the modified REST response.

Customizing the List Screen

The Swift template app created by forceios simply shows a list of names from a Salesforce organization. Customers can’t interact—they can only view the names. Let’s spice up a forceios Swift app by adding support for a left swipe gesture. When this gesture occurs, your code intercepts it and tells iOS that it indicates a delete request. iOS then adds a Delete button to the right end of the swiped row. If the user taps this button, your code responds as follows:
  1. When the user taps the Delete button or swipes far left:
    1. Send a REST request to delete the associated Salesforce record.
    2. Remove the row from the list view.
    3. Remove the row’s data from your app’s internal storage.
  2. If the deletion REST request succeeds, reload the table view’s data.
  3. If the deletion REST request fails:
    • Alert the user to the error.
    • Reinstate the removed row in the list view and in your app’s internal storage.

Change the Default REST Request

Your app’s table view gets its values from a simple SOQL SELECT query. By default, this query requests User records. You can’t do much with User records, though, so let’s change User to Contact.

  1. In Xcode, open the “MyTrailNativeApp” workspace if it’s not still open.
  2. In Project Navigator, select the RootViewController.swift file.
  3. Scroll to the loadView() method.
  4. Change the SOQL query to the following:
    SELECT Name, Id FROM Contact LIMIT 10

Add the Delete Button Protocol Methods

To intercept the swipe and add the Delete button, you override a couple of UITableViewController methods.

  1. In the RootViewController class, stub in these methodsUITableViewController.
    override func tableView(_ tableView: UITableView, 
        editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle 
    {
            
    }
    
    override func tableView(_ tableView: UITableView, 
        commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) 
    {
            
    }
  2. Add the following boilerplate implementation code for the first of these methods:
    override func tableView(_ tableView: UITableView, 
        editingStyleForRowAt indexPath: IndexPath) -> UITableViewCellEditingStyle 
    
    {
        if (indexPath.row < self.dataRows.count) {
           return UITableViewCellEditingStyle.delete
        } else {
           return UITableViewCellEditingStyle.none
        }
    }

Let’s analyze what happens in this code. When iOS detects the user’s swipe left gesture, it calls the tableView(_:editingStyleForRowAt:) method to find out which type of edit the user wants. To indicate a delete request, you return UITableViewCellEditingStyle.delete. On left swipe, iOS then displays a Delete button at the right end of the row.

Obviously, you don’t want to try to delete something that’s not there. How do you detect that the swipe fell on an empty row? You can compare the number of records in the data set to the zero-based index of the swiped row. If the index is greater than or equal to the number of records the row is empty. You then return UITableViewCellEditingStyle.none to tell iOS to ignore the request.

Notice that the code reads count from the dataRows object. In the template app, RootViewController defines this object to contain records returned by successful REST responses. Do you remember how RootViewController uses dataRows to populate the table view?

Set Up the Custom Editing Action

The second UITableViewController override method performs custom actions for the editing operation you specified. In this method, you create and send a REST request to delete the Salesforce record behind the swiped row. Since the REST mechanism runs asynchronously, you can’t know whether the request succeeds or fails until the response arrives. You can guard against failure, though.

It’s possible that a quick-fingered user could delete multiple rows before the first REST response arrives. If one of the requests fails, how can you reinstate the deleted row? One way is to cache information for each deleted row in a dictionary. To make this dictionary accessible across method calls, you can create a property.

  1. Near the top of the RootViewController class, declare the following struct and a variable that uses it.
    class RootViewController : UITableViewController
    {
        var dataRows = [Dictionary<String, Any>]()
    
        struct DeletedItemInfo {
            var data: [String:Any]
            var path: IndexPath
        }
        private var deleteRequests = [Int:DeletedItemInfo]()
  2. In the tableView(_:commit:forRowAt:) method, declare a row variable to track the original location of the deleted item.
    override func tableView(_ tableView: UITableView, 
                    commit editingStyle: UITableViewCellEditingStyle, 
                     forRowAt indexPath: IndexPath) 
    {
        let row = indexPath.row
  3. Add an if block. For the if conditions, make sure that the editing style indicates a deletion, and also use the row < count test.
    override func tableView(_ tableView: UITableView, 
                    commit editingStyle: UITableViewCellEditingStyle, 
                     forRowAt indexPath: IndexPath) 
    {
        let row = indexPath.row
        
        if (row < self.dataRows.count && editingStyle == UITableViewCellEditingStyle.delete)
        {
        }
    }
    In this conditional block, you add code to cache the recovery data.
  4. Inside the if block, obtain the selected record’s ID from the associated dataRows object.
    let deletedId: String = self.dataRows[row]["Id"] as! String
  5. Create a constant DeletedItemInfo object named deletedItemInfo, and store the selected row and its index path in it. (Later, you add this object to the deleteRequests dictionary.)
    let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)

Here’s the if block in its current state:

if (row < count && editingStyle == UITableViewCellEditingStyle.delete)
{
    let deletedId: String = self.dataRows[row]["Id"] as! String
    let deletedItemInfo = DeletedItemInfo(data:self.dataRows[row], path:indexPath)
}

Delete the Entry from Salesforce, the Table View, and the Data Source

All that’s left to do in the list screen is to send a DELETE request to the server.

When you create the REST request, you use its ID as the key for an object in the deleteRequests dictionary. Later, you can reuse the same ID to retrieve information on the deleted item.

iOS enforces an important protocol here: if you delete a row from a table view, you’re required to delete the corresponding entry from the data source within the same code block. Therefore, you dispose of both the row and the data source entry within this method.

  1. Continuing at the end of the previous if block, provide a constant that references the shared instance of RestClient.
    let restClient = RestClient.shared
  2. Create a RestRequest object for deleting the Salesforce object. To create the request, call the restClient.requestForDelete method, using the “Contact” object type and the deletedId value. These two values indicate exactly which Salesforce record to delete.
    let restClient = RestClient.shared
    
    let deleteRequest: RestRequest = 
        restClient.requestForDelete(withObjectType: "Contact", objectId: deletedId)
  3. To send the request to Salesforce, call the RestClient send(request: onFailure: onSuccess:) block function.
    let restClient = RestClient.shared
    
    let deleteRequest: RestRequest = 
        restClient.requestForDelete(withObjectType: "Contact", objectId: deletedId)
        restClient.send(request: deleteRequest, onFailure: { (error, urlResponse) in
    
        }) { [weak self] (response, urlResponse) in
    
        }
    Note

    Note

    Wondering what happened to the onSuccess: parameter? This code uses trailing closure syntax. To learn more about trailing closure syntax, see “Closures” in The Swift Programming Language (Swift 4.2).

  4. In the onFailure: block, log an appropriate error message. We’ll return to this block later to enhance the error handling.
    let restClient = RestClient.shared
    
    let deleteRequest: RestRequest = 
        restClient.requestForDelete(withObjectType: "Contact", objectId: deletedId)
        restClient.send(request: deleteRequest, onFailure: { (error, urlResponse) in
            os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
        }) { [weak self] (response, urlResponse) in
    
        }
  5. For the success handler, we use the trailing closure to update the app’s list of contacts. First, add a block that runs on the main thread. You use such a block for any UI updates that occur asynchronously.
    }) { [weak self] (response, urlResponse) in
        DispatchQueue.main.async {
                    
        }
    }
  6. In the DispatchQueue.main.async block, cache the deletedItemInfo array in the deleteRequests dictionary. Use the RestRequest object’s ID—request.hashValue—as the key.
    }) { [weak self] (response, urlResponse) in
        DispatchQueue.main.async {
            self?.deleteRequests[request.hashValue] = deletedItemInfo
                    
        }
    }
  7. To update the display, delete the row from the dataRows object and reload the table view.
    }) { [weak self] (response, urlResponse) in
        DispatchQueue.main.async {
            self?.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
            self?.dataRows.remove(at: row)
            self?.tableView.reloadData()
        }
    }
And you’re done. Here’s the tableView(_:commit:forRowAt:) method up to this point.
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
    
    let row = indexPath.row
    if (row < self.dataRows.count && editingStyle == UITableViewCellEditingStyle.delete)
    {
        let deletedId: String = self.dataRows[row]["Id"] as! String
        let deletedItemInfo = DeletedItemInfo(data: self.dataRows[row], path:indexPath)
        let restClient = RestClient.shared
        
        let deleteRequest: RestRequest = 
            restClient.requestForDelete(withObjectType: "Contact", objectId: deletedId)
        restClient.send(request: deleteRequest, onFailure: { (error, urlResponse) in
            os_log("\nError invoking: %@", log: .default, type: .debug, deleteRequest)
        }) { [weak self] (response, urlResponse) in
            DispatchQueue.main.async {
                self?.deleteRequests[deleteRequest.hashValue] = deletedItemInfo
                self?.dataRows.remove(at: row)
                self?.tableView.reloadData()
            }
        }
    }
}

Add the Final Touches

Great! You’ve added some user interaction to your new app. It’s only a button that deletes a record on the server, but, for this bare-bones template, that’s a sea change. Now, let’s tackle the problem of dealing with failure—not universal existential failure, but how to recover when our delete request doesn’t work.

By the time you learn that the delete failed, you’ve already removed the deleted record from both the dataRows array and the table view. Clearly, you must backtrack and reinstate those items, but how? Luckily, you’ve prepared well. Remember that you cache every DELETE REST request object in the deleteRequests dictionary? Smart thinking! Since each REST error returns the pertinent RestRequest object, you can easily find the deleted content in the deleteRequests dictionary.

Define a reinstateDeleteRowWithRequest(_:) Method

  1. At the end of the RootViewController class definition, add a reinstateDeleteRowWithRequest(request:) method with an empty implementation block.
    func reinstateDeletedRowWithRequest(_ request:RestRequest) {
    
    }
  2. Since this method changes UI elements, start with a block that ensures that all calls occur on the main thread.
    func reinstateDeletedRowWithRequest(_ request:RestRequest) {
        DispatchQueue.main.async {
            
        }
    }
  3. In the DispatchQueue.main.async block, use the request argument’s ID to look up the deleted data in the deleteRequests dictionary. The rest of this method applies only if this value is not nil.
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
    
        }
    }
  4. If you successfully retrieved the deleted row values, reinsert the data object at index 0 of the dataRows dictionary.
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
            // the beginning of the dataRows dictionary (index 0).
            self.dataRows.insert(rowValue.data, at: 0)
    
        }
    }
  5. To reinstate the deleted contact name, reload the data of the UITableView.
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
            // the beginning of the dataRows dictionary (index 0).
            self.dataRows.insert(rowValue.data, at: 0)
            self.tableView.reloadData()
    
        }
    }
  6. Now that the contact is undeleted, remove its entry from the deleteRequests array.
    DispatchQueue.main.async {
        if let rowValues = self.deleteRequests[request.hashValue] {
            // the beginning of the dataRows dictionary (index 0).
            self.dataRows.insert(rowValue.data, at: 0)
            self.tableView.reloadData()
            self.deleteRequests.removeValue(forKey: request.hashValue as Int)
        }
    }

    Here’s the finished reinstateDeletedRowWithRequest(_:) method:

    func reinstateDeletedRowWithRequest(_ request:RestRequest)
    {
        // Reinsert deleted rows if the operation is DELETE and the ID matches the deleted ID.
        // The trouble is, the NSError parameter doesn't give us that info, so we can't really
        // judge which row caused this error.
    
        DispatchQueue.main.async {
            if let rowValue = self.deleteRequests[request.hashValue] {
                // the beginning of the dataRows dictionary (index 0).
                self.dataRows.insert(rowValue.data, at: 0)
                self.tableView.reloadData()
                self.deleteRequests.removeValue(forKey: request.hashValue as Int)
            }
        }
    }

Handle REST Response Errors

A REST response can indicate success or failure. In our case, success means that the row was successfully deleted. Failure means that the row could not be deleted, or that the connection dropped or timed out—in these cases, the network responds with an error.

You can now use your new reinstateDeleteRowWithRequest(_:) method by calling it when a REST response indicates an error. In this Swift app, you use the onFailure closure of the RestClient.send() method to handle REST response errors. This block replaces the three Objective-C RestClientDelegate error handlers.

For this app, you call reinstateDeleteRowWithRequest(_:) regardless of the error type. You also display the error message to the user in an alert box. To make the alert box code reusable, let’s put it in its own method body.

  1. In the RootViewController class body, create a new private method named showErrorAlert. This method takes two arguments: The NSError and RestRequest objects that are passed to the catch block.
    private func showErrorAlert(_ error: NSError, request: RestRequest) {
    
    }
  2. Because this function performs an asynchronous UI task, start by creating a DispatchQueue.main.async block in the method body.
    private func showErrorAlert(_ error: NSError, request: RestRequest) {
        DispatchQueue.main.async {
    
        }
    }
  3. In the DispatchQueue.main.async block, create and show an alert box. For the alert box string, use the message member of the error.userInfo array. This code looks a trifle messy—that’s because userInfo is app-defined and optional. Before using it, you make sure that the caught error contains a userInfo array with the expected structure.
    private func showErrorAlert(_ error: NSError, request: RestRequest) {
        DispatchQueue.main.async {
            let errArray = error.userInfo["error"] as! [Any]
            if errArray.count > 0 {
                let dictionary =  errArray[0] as! [String:Any]
                let message = (dictionary["message"] as? String) ?? 
                    "Failed to delete item"
                let title = "Cannot delete item"
                let alert = UIAlertController(title: title, message:
                    message, preferredStyle: UIAlertControllerStyle.alert)
                alert.addAction(
                    UIAlertAction(title: NSLocalizedString("OK", comment: ""), 
                    style: UIAlertActionStyle.default, handler: nil))
                self.present(alert, animated: true, completion: nil)
            }
        }
    }
  4. In the tableView(_:commit:forRowAt:) method, call the new methods in the onFailure closure of the RestClient send() method.
    restClient.send(request: deleteRequest, onFailure: { (error, urlResponse) in
        SalesforceLogger.d(type(of:self), message:"Error invoking: \(deleteRequest)")
        self.reinstateDeletedRowWithRequest(deleteRequest)
        self.showErrorAlert(error! as NSError,request: deleteRequest)
    }) { [weak self] (response, urlResponse) in
    ...

Try It Out!

That’s it! You’re ready to build and run your code in the iOS simulator. Notice that you get an error response when you try to delete any default contact in the Developer Edition database. These errors occur because each contact that comes pre-packaged in a Developer Edition org is the parent of other records. To prepare for testing, log in to your Developer Edition org and create one or more test contacts that don’t own other records.

Resources

retargeting