Getting the Data List

The user can list the entered income and expense entries and edit entries in the data listing screen. This topic explains the data listing process and the next topic explains the data editing process.

Class structure

The data listing screen consists of the classes shown in the class diagram below. The central class is the BalanceListViewController class that inherits the UIViewController class. It also implements the UITableViewDelegate protocol and the UITableViewDataSource protocol for handling the table view.

The BalanceListViewController class accesses Kii Cloud and gets income and expense data as an array of KiiObjects. The obtained KiiObjects are stored in a variable of a BalanceListViewController instance.

The TotalAmountTableViewCell class and the BalanceItemTableViewCell class are used for handling cells in the table view. These classes simply store fields in a cell as outlets.

Check the source code below.

Swift

Objective-C

Getting the data list

In the data listing screen, KiiObjects that store income and expense data are obtained from Kii Cloud.

The following two methods are mainly used for getting data from Kii Cloud. Kii Balance uses the first method.

  • Querying KiiObjects in a specific bucket

    You get KiiObjects that match certain conditions in a batch. This method is suitable for the data listing screen.

  • Specifying the ID of a target KiiObject

    You save the ID of a certain KiiObject and get the KiiObject with the saved ID. This method is not suitable for the data listing screen because ID management is required.

Data is obtained within the viewDidLoad() method. This method is called after the view is loaded.

The getItems() method actually gets data. See how the query is started in the code block below.

  • override func viewDidLoad() {
      super.viewDidLoad()
      ......
      self.getItems()
    }
    
    func getItems() {
      let bucket = KiiUser.current()!.bucket(withName: BalanceItem.Bucket)
    
      // Create a query instance.
      let query = KiiQuery.init(clause: nil)
      // Sort KiiObjects by the _created field.
      query.sort(byAsc: BalanceItem.FieldCreated)
    
      let progress = KiiProgress.create(message:"Loading...")
      self.present(progress, animated: false, completion: nil)
    
      var allResults: [KiiObject] = []
    
      // Define a recursive closure to get all KiiObjects.
      var callback: KiiQueryResultBlock?
      callback = { (query: KiiQuery?, bucket: KiiBucket, results: [Any]?, nextQuery: KiiQuery?, error: Error?) -> Void in
        ......
      }
      bucket.execute(query, with: callback!)
    }
  • - (void)viewDidLoad {
      [super viewDidLoad];
      ......
      [self getItems];
    }
    
    - (void) getItems {
      KiiBucket *bucket = [[KiiUser currentUser] bucketWithName:BalanceItemBucket];
    
      // Create a query instance.
      KiiQuery *query = [KiiQuery queryWithClause:nil];
      // Sort KiiObjects by the _created field.
      [query sortByAsc:BalanceItemFieldCreated];
    
      UIAlertController *progress = [KiiProgress createWithMessage:@"Loading..."];
      [self presentViewController:progress animated:NO completion:nil];
    
      NSMutableArray *allResults = [[NSMutableArray alloc] init];
    
      // Define a recursive block to get all KiiObjects.
      __block KiiQueryResultBlock callback = ^(KiiQuery *query, KiiBucket *bucket, NSArray *results, KiiQuery *nextQuery, NSError *error) {
        ......
      };
      [bucket executeQuery:query withBlock:callback];
    }

The query gets all KiiObjects in the bucket in the ascending order of the creation time. The query is performed in the same way as that in Hello Kii. For more information, see the Hello Kii tutorial.

In order to pagenate the query result, the closure or block of the execute(_:with:) method is not directly written as an argument but specified in a variable callback. The types of arguments passed to the method are substantially the same as that in Hello Kii. For pagination, see the next section.

Pagination

Kii Balance provides a complete data listing feature by paginating the query result.

Pagination is the process of dividing the query result into pages and getting one page at a time, when the query result contains numerous items.

For example, if a query gets 420 KiiObjects, the result can be obtained in multiple pages as below.

You can get up to 200 entries in one page because the default value of the bestEffortLimit option is 200. However, as shown in the second result, various conditions such as the transfer size can decrease the number of returned entries even if there is another or more pages to get. Additionally, the last page might contain no entry.

The APIs in the above figure return values as follows.

  • For the first page

    The execute(_:with:) method of the KiiBucket class gets the first page (:num1:). The query result is obtained in the results argument in the callback as an array of KiiObjects. nextQuery contains a query to get the next page because there are more pages to get.

  • For the second page

    The execute(_:with:) method gets the second page by using nextQuery that was returned when the first page was obtained (:num2:). As with the first query, the query result is obtained in the results argument. nextQuery contains a query to get the next page because there is another page to get.

  • For the third page

    The execute(_:with:) method gets the third page by using nextQuery that was returned when the second page was obtained (:num3:). nextQuery contains a value of nil. It means there is no more page and all the entries are obtained.

See how pagination is performed in the code block below. This block shows how the query that was started in the previous sample code is executed.

  • func getItems() {
      ......
      let progress = KiiProgress.create(message:"Loading...")
      self.present(progress, animated: false, completion: nil)
    
      var objectList: [KiiObject] = []
    
      // Define a recursive closure to get all KiiObjects.
      var callback: KiiQueryResultBlock?
      callback = { (query: KiiQuery?, bucket: KiiBucket, results: [Any]?, nextQuery: KiiQuery?, error: Error?) -> Void in
        if error != nil {
          progress.dismiss(animated: true, completion: {
            let description = (error! as NSError).userInfo["description"] as! String
            let alert = KiiAlert.create(title: "Error", message: description)
            self.present(alert, animated: true, completion: nil)
          })
          callback = nil
          return
        }
        objectList.append(contentsOf: results as! [KiiObject])
    
        // Check if more KiiObjects exit.
        if nextQuery == nil {
          progress.dismiss(animated: true, completion: nil)
          self.navigationItem.rightBarButtonItem = self.addButton
          self.items = objectList
          self.tableView.reloadData()
          callback = nil
        } else {
          // Get the remaining KiiObjects.
          bucket.execute(nextQuery, with: callback!)
        }
      }
      // Call the Kii Cloud API to query KiiObjects.
      bucket.execute(query, with: callback!)
    }
  • - (void) getItems {
      ......
      UIAlertController *progress = [KiiProgress createWithMessage:@"Loading..."];
      [self presentViewController:progress animated:NO completion:nil];
    
      NSMutableArray *objectList = [[NSMutableArray alloc] init];
    
      // Define a recursive block to get all KiiObjects.
      __block KiiQueryResultBlock callback = ^(KiiQuery *query, KiiBucket *bucket, NSArray *results, KiiQuery *nextQuery, NSError *error) {
        if (error != nil) {
          [progress dismissViewControllerAnimated:YES completion:^{
            UIAlertController *alert = [KiiAlert createWithTitle:@"Error" andMessage:error.description];
            [self presentViewController:alert animated:YES completion:nil];
          }];
          callback = nil;
          return;
        }
        [objectList addObjectsFromArray:results];
    
        // Check if more KiiObjects exit.
        if (nextQuery == nil) {
          [progress dismissViewControllerAnimated:YES completion:nil];
          self.navigationItem.rightBarButtonItem = self.addButton;
          self.items = objectList;
          [self.tableView reloadData];
          callback = nil;
        } else {
          // Get the remaining KiiObjects.
          [bucket executeQuery:nextQuery withBlock:callback];
        }
      };
      // Call the Kii Cloud API to query KiiObjects.
      [bucket executeQuery:query withBlock:callback];
    }

Note the following key points of the above processing.

  • Pagination is implemented by recursively calling KiiQueryResultBlock.
    • The Swift version recursively calls the closure. Swift does not allow you to reference a variable that holds a closure when you define the variable. In order to implement the recursion, the code above declares the variable callback in the var statement, then in separate lines, sets the closure to it and references the variable.
    • The Objective-C version recursively calls the block. In order to implement the recursion, the code above declares the variable callback with the __block keyword so that the variable can be referenced when the block is defined.
  • The KiiProgress class displays the progress. The progress indicator disappears when an error occurs and the mobile app stops processing the subsequent pages or when all the entries are obtained.
  • In order to recover from a possible error, obtained KiiObjects are not directly added to self.items. The query result is saved in objectList, then added to self.items in a batch when all the entries are obtained. Suppose KiiObjects were directly added to self.items. If an error occurred while getting the second page, only the entries obtained in the first page would remain in self.items.

In order to ensure the quality of your mobile app, pay attention to the recovery processing mentioned in the last key point. Instabilities such as signal quality on the smartphone cause API errors because network access is required for executing the Kii Cloud APIs.

When designing your mobile app, you need to not only create error messages but also consider how the user can recover from errors. If your scenarios are insufficient, the user might encounter data inconsistency and/or find no way to operate the mobile app. You can test the error case mentioned in the last key point by decreasing the page size in the limit property of query and using a device in airplane mode in combination with breakpoints of the debugger.

If the list is obtained successfully, the obtained KiiObjecst are added to self.items under the control of the BalanceListViewController instance. Then the "Refresh" button is replaced with the "Add" button in the navigation bar. If the list cannot be obtained, the user is supposed to tap the "Refresh" button to get the list.

In Kii Balance, pagination is implemented with the non-blocking API. In some cases, your implementation can be simpler by using Grand Central Dispatch (GCD) and the blocking API.

Preventing a memory leak in a callback

It is necessary to take measures against a memory leak in the recursion processing for pagination because a retain cycle occurs in the KiiQueryResultBlock closure.

A query is executed with references to various objects. In this section, let us focus on the BalanceListViewController instance, the getItems() method, and the KiiQueryResultBlock closure. As shown in the figure below, all of them are strongly referenced.

Reference :labelc: causes a retain cycle. Without any countermeasure, a memory leak would occur.

The closure implementation uses the variable callback for recursive calls. The closure captures a strong reference to this variable and creates a retain cycle to itself.

Kii Balance resolves the retain cycle by breaking the reference. To do so, a value of nil is set to callback after an error occurs or all entries are obtained.

The other references in the figure do not cause any retain cycle nor memory leak.

  • Reference :labela:

    The local variable callback in the getItems() method has a strong reference to the closure. This reference does not cause any memory leak because callback is deallocated when the getItems() method completes.

  • Reference :labelb:

    The closure has a strong reference to the BalanceListViewController instance because the closure references self.

    If you search information about referencing self from a closure on the Internet, you might find articles about retain cycles. Note that the implementation of Kii Balance does not cause any retain cycle. The BalanceListViewController instance does not have any instance variable that holds a reference to the closure and there is only the one-way reference from the closure to the BalanceListViewController instance. In this case, no memory leak is caused.

    If the closure were managed in an instance variable of the BalanceListViewController instance, it would cause a retain cycle. To avoid a retain cycle, self in the closure should be weakly referenced.

Displaying data in the table view

The tableView(_:cellForRowAt:) method creates and returns a cell to display an entry.

As with Hello Kii, this method processes data stored in a KiiObject instance for display and returns a cell for the entry according to the specification of the table view.

See how cells are created in Kii Balance in the code block below.

  • func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      if indexPath.section == 0 {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Total", for: indexPath) as! TotalAmountTableViewCell
        let total = self.calcTotal()
        if total < 0 {
          cell.amountLabel.textColor = UIColor.red
        } else {
          cell.amountLabel.textColor = UIColor.black
        }
        cell.amountLabel.text = self.formatCurrency(amount: total)
        return cell
      } else {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! BalanceItemTableViewCell
        let obj = self.items[indexPath.row]
        let name = obj.getForKey(BalanceItem.FieldName) as! String
        let type = (obj.getForKey(BalanceItem.FieldType) as! NSNumber).intValue
        let amount = (obj.getForKey(BalanceItem.FieldAmount) as! NSNumber).intValue
    
        let formatter = DateFormatter()
        formatter.dateStyle = .medium
        let date = formatter.string(from: obj.created!)
        cell.dateLabel.text = date
    
        var amountDisplay: Double
        cell.nameLabel.text = name
        if type == BalanceItemType.Income.rawValue {
          cell.amountLabel.textColor = UIColor.black
          amountDisplay = Double(amount) / 100.0
        } else {
          cell.amountLabel.textColor = UIColor.red
          amountDisplay = -1 * Double(amount) / 100.0
        }
        cell.amountLabel.text = self.formatCurrency(amount: amountDisplay)
        return cell;
      }
    }
  • - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
      if (indexPath.section == 0) {
        TotalAmountTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Total" forIndexPath:indexPath];
        double total = [self calcTotal];
        if (total < 0) {
          cell.amountLabel.textColor = [UIColor redColor];
        } else {
          cell.amountLabel.textColor = [UIColor blackColor];
        }
        cell.amountLabel.text = [self formatCurrency: total];
        return cell;
      } else {
        BalanceItemTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
        KiiObject *obj = self.items[indexPath.row];
        NSString *name = [obj getObjectForKey:BalanceItemFieldName];
        NSNumber *type = [obj getObjectForKey:BalanceItemFieldType];
        int amount = [(NSNumber*)[obj getObjectForKey:BalanceItemFieldAmount] doubleValue];
    
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        formatter.dateStyle = NSDateFormatterMediumStyle;
        cell.dateLabel.text = [formatter stringFromDate:obj.created];
    
        double amountDisplay;
        cell.nameLabel.text = name;
        if ([type intValue] == BalanceItemTypeIncome) {
          cell.amountLabel.textColor = [UIColor blackColor];
          amountDisplay = amount / 100.0;
        } else {
          cell.amountLabel.textColor = [UIColor redColor];
          amountDisplay = -1 * amount / 100.0;
        }
        cell.amountLabel.text = [self formatCurrency: amountDisplay];
        return cell;
      }
    }

This code associates each field of a KiiObject instance with a display item.

The TotalAmountTableViewCell class is used to create a cell for the balance that is displayed at section 0 of the table view. The BalanceItemTableViewCell class is used to cells for the list of income and expense items displayed at the subsequent sections.


What's Next?

Let us review how to edit entries in the data listing screen.

Go to Editing an Entry.