Using SmartStore in Swift Apps

You can easily install the basic plumbing for SmartStore in a forceios native Swift project.
In this example, you create a SmartStore soup and upsert the queried list of contact names into that soup. You then change the Swift template app flow to populate the table view from the soup instead of directly from the REST response. If you’re not familiar with Xcode project structure, consult the Xcode Help.
  1. Using forceios, create a native Swift project similar to the following example:
    1$ forceios create
    2Enter your application type (native_swift or native, leave empty for native_swift): <Press RETURN>
    3Enter your application name: <Enter any name you like>
    4Enter your package name: com.myapps.ios
    5Enter your organization name (Acme, Inc.): MyApps.com
    6Enter output directory for your app (leave empty for the current directory): <Press RETURN or enter a directory name>
  2. In your project’s root directory, create a userstore.json file with the following content.
    1{ "soups": [
    2    {
    3    "soupName": "Contact",
    4    "indexes": [
    5        { "path": "Name", "type": "string"},
    6        { "path": "Id", "type": "string"}                        
    7        ]
    8    }    
    9]}
  3. Open your app's .xcworkspace file in Xcode.
  4. Add your configuration file to your project.
    1. In the Xcode Project navigator, select the project node.
    2. In the Editor window, select Build Phases.
    3. Expand Copy Bundle Resources.
    4. Click + (”Add items”).
    5. Select your soup configuration file. If your file is not already in an Xcode project folder:
      1. To select your file in Finder, click Add Other....
      2. Click Open, then click Finish.
  5. In your project’s source code folder, select Classes/AppDelegate.swift.
  6. In the application(_:didFinishLaunchingWithOptions:) callback method, load userstore.json definitions in the call to AuthHelper.loginIfRequired.
    1func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
    2{
    3    self.window = UIWindow(frame: UIScreen.main.bounds)
    4    self.initializeAppViewState();
    5    ...
    6
    7    AuthHelper.loginIfRequired { [weak self] in
    8        MobileSyncSDKManager.shared.setupUserStoreFromDefaultConfig()
    9        self?.setupRootViewController()
    10    }
    11    return true
    12}
    Your app is now set up to load your SmartStore configuration file at startup. This action creates the soups you specified as empty tables. Let's configure the RootViewController class to use SmartStore.
  7. In RootViewController.swift, import SmartStore:
    1import SmartStore
  8. At the top of the RootViewController class, declare a variable for a SmartStore instance.
    1class RootViewController : UITableViewController
    2{
    3    var dataRows = [NSDictionary]()
    4    var store = SmartStore.shared(withName: SmartStore.defaultStoreName)
  9. On the next line, declare a constant that defines an OSLog component.
    1class RootViewController : UITableViewController
    2{
    3    var dataRows = [NSDictionary]()
    4    var store = SmartStore.shared(withName: SmartStore.defaultStoreName)
    5    let mylog = OSLog(subsystem: "com.testapp.swift", category: "tutorial")
  10. In the loadView() method, find the call to .query and add the Id field to the SOQL statement.
    1let request = RestClient.shared.request(forQuery: "SELECT Name, Id FROM Contact LIMIT 10")
  11. In the handleSuccess(_:_:) method, immediately after the guard block, add the following code.
    1func handleSuccess(response: RestResponse, request: RestRequest) {
    2    guard let jsonResponse  = try? response.asJson() as? [String:Any], 
    3        let records = jsonResponse["records"] as? [[String:Any]]  else {
    4        SalesforceLogger.d(RootViewController.self, message:"Empty Response for : \(request)")
    5        return
    6    }
    7
    8    if let smartstore = self.store, 
    9       smartstore.soupExists(forName: "Contact") {
    10            smartstore.clearSoup("Contact")
    11            smartstore.upsert(entries: records, forSoupNamed: "Contact")
    12            os_log("\nSmartStore loaded records.", log: self.mylog, type: .debug)
    13       }
    14
    15
    16    SalesforceLogger.d(type(of:self), message:"Invoked: \(request)")
    17    DispatchQueue.main.async {
    18        self.dataRows = records
    19        self.tableView.reloadData()
    20    }
    21}// end of handleSuccess method
    This code checks whether the Contact soup exists. If the soup exists, the code clears all data from the soup, and then upserts the retrieved records.
  12. Launch the app, then check your work using the Dev Tools menu.
    1. To bring up the menu, type control + command + z if you’re using the iOS emulator, or shake your iOS device.
    2. Click Inspect SmartStore.
    3. To list your Contact soup and number of records, click Soups.

      If you get a "Query: No soups found" message, chances are you have an error in your userstore.json file.

      Note

You’ve now created and populated a SmartStore soup. However, at this point your soup doesn’t actually serve a purpose. Let's make it more useful by populating the list view from SmartStore records rather than directly from the REST response.
  1. After the handleSuccess(_:_:) method, add a method named loadFromStore().
    1func loadFromStore() {
    2
    3}
  2. In loadFromStore(), define an if block that builds a Smart SQLquery specification as its first condition. Configure the query to extract the first 10 Name values from the Contact soup.
    1func loadFromStore() {
    2    if let querySpec = QuerySpec.buildSmartQuerySpec(
    3        smartSql: "select {Contact:Name} from {Contact}", pageSize: 10),
    4    
    5}
  3. Add a second condition that verifies the SmartStore handle and a third condition that runs the SmartStore query. Since the query method throws an exception, call it from a do...try...catch block.
    1func loadFromStore() {
    2    if let querySpec = QuerySpec.buildSmartQuerySpec(
    3        smartSql: "select {Contact:Name} from {Contact}", pageSize: 10),    
    4        let smartStore = self.store,
    5        let records = try? smartStore.query(using: querySpec, 
    6            startingFromPageIndex: 0) as? [[String]] {
    7
    8    }
    9    
    10}
  4. Transfer the names returned by the SmartStore query to the view’s dataRows member .
    1func loadFromStore() {
    2    if let querySpec = QuerySpec.buildSmartQuerySpec(
    3        smartSql: "select {Contact:Name} from {Contact}", pageSize: 10),    
    4        let smartStore = self.store,
    5        let records = try? smartStore.query(using: querySpec, 
    6            startingFromPageIndex: 0) as? [[String]] {
    7        self.dataRows = records.map({ row in
    8            return ["Name": row[0]]
    9        })
    10    }
    11}
  5. Using the DispatchQueue system object, switch to the main thread and refresh the view’s displayed data.
    1func loadFromStore() {
    2    if let querySpec = QuerySpec.buildSmartQuerySpec(
    3        smartSql: "select {Contact:Name} from {Contact}", pageSize: 10),
    4        let smartStore = self.store,
    5        let records = try? smartStore.query(using: querySpec, 
    6            startingFromPageIndex: 0) as? [[String]] {
    7        self.dataRows = records.map({ row in
    8            return ["Name": row[0]]
    9        })
    10        DispatchQueue.main.async {
    11            self.tableView.reloadData()
    12        }
    13    }
    14}
  6. Scroll back to the handleSuccess(_:_:) method and remove the existing code that reloads the view’s data.
    1func handleSuccess(response: RestResponse, request: RestRequest) {
    2    guard let jsonResponse  = try? response.asJson() as? [String:Any], 
    3        let records = jsonResponse["records"] as? [[String:Any]]  else {
    4        SalesforceLogger.d(RootViewController.self, message:"Empty Response for : \(request)")
    5        return
    6    }
    7
    8    if ((self.store.soupExists(forName: "Contact"))) {
    9        self.store.clearSoup("Contact")
    10        self.store.upsert(entries: records, forSoupNamed: "Contact")
    11        os_log("\nSmartStore loaded records.", log: self.mylog, type: .debug)
    12    }
    13
    14    // Remove the following lines
    15    SalesforceLogger.d(type(of:self), message:"Invoked: \(request)")
    16    DispatchQueue.main.async {
    17        self.dataRows = records
    18        self.tableView.reloadData()
    19    }
    20}// end of handleSuccess method
  7. Using self, call your new loadFromStore() method immediately after the upsert(entries:forSoupNamed:) call.
    1func handleSuccess(response: RestResponse, request: RestRequest) {
    2    guard let jsonResponse  = try? response.asJson() as? [String:Any], 
    3        let records = jsonResponse["records"] as? [[String:Any]]  else {
    4        SalesforceLogger.d(RootViewController.self, message:"Empty Response for : \(request)")
    5        return
    6    }
    7
    8    if ((self.store.soupExists(forName: "Contact"))) {
    9        self.store.clearSoup("Contact")
    10        self.store.upsert(entries: records, forSoupNamed: "Contact")    
    11        self.loadFromStore()
    12        os_log("\nSmartStore loaded records.", log: self.mylog, type: .debug)
    13    }
    14} // end of loadView
When you retest your app, you see that the table view is populated as before, but from SmartStore rather than a live REST response. In the real world, you'd create an editing interface for the Contact list, and then upsert your customers' edits to SmartStore. The customer could then continue working on the Contact list even if the mobile device lost connectivity. When connectivity is restored, you could then merge the customer’s work to the server—and also resync SmartStore—using Mobile SDK.